diff --git a/Dockerfile b/Dockerfile index 955b9ba..8a65dec 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,5 +9,5 @@ RUN export COMPOSER_HOME=/var/www/.composer \ && mv .env.example .env \ && FORCE_PERMS=1 NGINX_START=FALSE /sbin/init \ && chmod +x /var/www/html/artisan \ - && touch .migrate \ + && /var/www/html/artisan storage:link \ && rm -rf /var/www/.composer diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index fc735a3..ce82446 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -72,6 +72,15 @@ class SearchController extends Controller $result->push(['name'=>sprintf('%s (%s)',$o->name,$o->service->sid),'value'=>'/u/service/'.$o->id,'category'=>'Broadband']); } + # Look for Domain Name + foreach (Service\Domain::Search($request->input('term')) + ->whereIN('account_id',$accounts) + ->orderBy('domain_name') + ->limit(10)->get() as $o) + { + $result->push(['name'=>sprintf('%s (%s)',$o->service_name,$o->service->sid),'value'=>'/u/service/'.$o->id,'category'=>'Domains']); + } + return $result; } } \ No newline at end of file diff --git a/app/Http/Controllers/ServiceController.php b/app/Http/Controllers/ServiceController.php index ef39597..fb0e2e3 100644 --- a/app/Http/Controllers/ServiceController.php +++ b/app/Http/Controllers/ServiceController.php @@ -3,18 +3,64 @@ namespace App\Http\Controllers; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; use Illuminate\View\View; use App\Models\Service; class ServiceController extends Controller { + /** + * Edit a domain service details + * + * @param Request $request + * @param Service $o + * @return \Illuminate\Http\RedirectResponse + */ + public function domain_edit(Request $request,Service $o) + { + session()->flash('service_update',TRUE); + + $validation = $request->validate([ + 'service.domain_name' => sprintf('required|unique:%s,domain_name,%d',$o->type->getTable(),$o->type->id), + 'service.domain_expire' => 'required|date', + 'service.domain_tld_id' => 'required|exists:ab_domain_tld,id', + 'service.domain_registrar_id' => 'required|exists:ab_domain_registrar,id', + 'service.registrar_account' => 'required', + 'service.registrar_username' => 'required|string|min:5', + 'service.registrar_ns' => 'required|string|min:5', + ]); + + $o->type->forceFill($validation['service'])->save(); + + return redirect()->back()->with('success','Record updated.'); + } + + /** + * List all the domains managed by the user + * + * @return View + */ + public function domain_list(): View + { + $o = Service\Domain::serviceActive() + ->serviceUserAuthorised(Auth::user()) + ->select('service_domains.*') + ->join('ab_service',['ab_service.id'=>'service_domains.service_id']) + ->with(['service.account','registrar']) + ->get(); + + return view('r.service.domain.list') + ->with('o',$o); + } + /** * Update a service * * @note: Route Middleware protects this path + * @param Request $request * @param Service $o - * @return View|void + * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|\Illuminate\Http\RedirectResponse */ public function update(Request $request,Service $o) { diff --git a/app/Interfaces/ServiceItem.php b/app/Interfaces/ServiceItem.php index ba95f36..7146c32 100644 --- a/app/Interfaces/ServiceItem.php +++ b/app/Interfaces/ServiceItem.php @@ -2,6 +2,8 @@ namespace App\Interfaces; +use Carbon\Carbon; + interface ServiceItem { /** @@ -11,6 +13,11 @@ interface ServiceItem */ public function getServiceDescriptionAttribute(): string; + /** + * Date the service expires + */ + public function getServiceExpireAttribute(): Carbon; + /** * Return the Service Name. * diff --git a/app/Models/DomainRegistrar.php b/app/Models/DomainRegistrar.php index 69a61ed..c650381 100644 --- a/app/Models/DomainRegistrar.php +++ b/app/Models/DomainRegistrar.php @@ -3,8 +3,11 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use Leenooks\Traits\ScopeActive; class DomainRegistrar extends Model { + use ScopeActive; + protected $table = 'ab_domain_registrar'; } \ No newline at end of file diff --git a/app/Models/DomainTld.php b/app/Models/DomainTld.php index 39ae4a8..666ebbe 100644 --- a/app/Models/DomainTld.php +++ b/app/Models/DomainTld.php @@ -3,13 +3,25 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use Leenooks\Traits\ScopeActive; class DomainTld extends Model { + use ScopeActive; + protected $table = 'ab_domain_tld'; + /* RELATIONS */ + public function services() { return $this->hasMany(Service::class); } + + /* ATTRIBUTES */ + + public function getNameAttribute($value): string + { + return strtoupper($value); + } } \ No newline at end of file diff --git a/app/Models/Service.php b/app/Models/Service.php index 6c717a8..ed0a042 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -24,8 +24,9 @@ use App\Traits\NextKey; * Services that belong to an account * * Attributes for services: - * + name_short : Service Product short name, eg: phone number, domain name, certificate CN - * + sid : System ID for service + * + billing_period : The period that this service is billed for by default + * + name_short : Service Product short name, eg: phone number, domain name, certificate CN + * + sid : System ID for service * * @package App\Models */ @@ -89,12 +90,19 @@ class Service extends Model implements IDs 'type', ]; + // @todo Change to self::INACTIVE_STATUS private $inactive_status = [ 'CANCELLED', 'ORDER-REJECTED', 'ORDER-CANCELLED', ]; + public const INACTIVE_STATUS = [ + 'CANCELLED', + 'ORDER-REJECTED', + 'ORDER-CANCELLED', + ]; + /** * Valid status shows the applicable next status for an action on a service * Each status can be @@ -362,6 +370,15 @@ class Service extends Model implements IDs }); } + /** + * Only query records that the user is authorised to see + */ + public function scopeAuthorised($query,User $uo) + { + return $query + ->whereIN($this->getTable().'.account_id',$uo->all_accounts()->pluck('id')->unique()->toArray()); + } + /** * Find inactive services. * @@ -1192,10 +1209,21 @@ class Service extends Model implements IDs return $this->active OR ($this->order_status AND ! in_array($this->order_status,$this->inactive_status)); } + /** + * Do we bill for this service + * + * @return bool + */ + public function isBilled(): bool + { + return ! ($this->external_billing && $this->suspend_billing); + } + /** * Should this service be invoiced soon * * @todo get the number of days from account setup + * @todo Use self::isBilled(); * @return bool */ public function isInvoiceDueSoon($days=30): bool @@ -1221,6 +1249,7 @@ class Service extends Model implements IDs * @param bool $future Next item to be billed (not in the next x days) * @return Collection * @throws Exception + * @todo Use self::isBilled(); */ public function next_invoice_items(bool $future,Carbon $billdate=NULL): Collection { diff --git a/app/Models/Service/Adsl.php b/app/Models/Service/Adsl.php index 31a5e12..6bbef46 100644 --- a/app/Models/Service/Adsl.php +++ b/app/Models/Service/Adsl.php @@ -92,6 +92,11 @@ class Adsl extends ServiceType implements ServiceItem,ServiceUsage return strtoupper($this->service_address) ?: 'NO Service Address'; } + public function getServiceExpireAttribute(): \Carbon\Carbon + { + // TODO: Implement getServiceExpireAttribute() method. + } + /** * Return the service number * diff --git a/app/Models/Service/Domain.php b/app/Models/Service/Domain.php index abf483a..339c00e 100644 --- a/app/Models/Service/Domain.php +++ b/app/Models/Service/Domain.php @@ -2,23 +2,41 @@ namespace App\Models\Service; -use App\Models\Base\ServiceType; -use App\Models\DomainRegistrar; -use App\Models\DomainTld; -use App\Interfaces\ServiceItem; -use App\Traits\NextKey; +use Carbon\Carbon; +use App\Models\Base\ServiceType; +use App\Models\{Account,DomainRegistrar,DomainTld,Service}; +use App\Interfaces\ServiceItem; +use App\Traits\{NextKey,ScopeServiceActive,ScopeServiceUserAuthorised}; + +/** + * Class Domain (Service) + * Services that domain names + * + * Attributes for services: + * + service_description : Description as shown in a Service Context + * + service_expire : The date the service expires + * + service_name : Name as shown in a Service Context + * + * @package App\Models\Service + */ class Domain extends ServiceType implements ServiceItem { - use NextKey; - const RECORD_ID = 'service__domain'; + use ScopeServiceActive,ScopeServiceUserAuthorised; protected $dates = [ 'domain_expire', ]; - protected $table = 'ab_service__domain'; + protected $table = 'service_domains'; protected $with = ['tld']; + /* RELATIONS */ + + public function account() + { + return $this->hasOneThrough(Account::class,Service::class); + } + public function registrar() { return $this->belongsTo(DomainRegistrar::class,'domain_registrar_id'); @@ -29,15 +47,46 @@ class Domain extends ServiceType implements ServiceItem return $this->belongsTo(DomainTld::class,'domain_tld_id'); } + /* SCOPES */ + + /** + * Search for a record + * + * @param $query + * @param string $term + * @return mixed + */ + public function scopeSearch($query,string $term) + { + // If we have a period in the name, we'll ignore everything after it. + $term = strstr($term,'.',TRUE) ?: $term; + + // Build our where clause + return parent::scopeSearch($query,$term) + ->orwhere('domain_name','like','%'.$term.'%'); + } + + /* ATTRIBUTES */ + public function getServiceDescriptionAttribute(): string { // N/A return 'Domain Name'; } + public function getServiceExpireAttribute(): Carbon + { + return $this->domain_expire; + } + + /** + * The name of the domain with its TLD + * + * @return string + */ public function getServiceNameAttribute(): string { - return sprintf('%s.%s',strtoupper($this->domain_name),strtoupper($this->tld->name)); + return strtoupper(sprintf('%s.%s',$this->domain_name,$this->tld->name)); } public function inContract(): bool diff --git a/app/Models/Service/Host.php b/app/Models/Service/Host.php index 59b7630..9594857 100644 --- a/app/Models/Service/Host.php +++ b/app/Models/Service/Host.php @@ -7,6 +7,7 @@ use App\Models\Base\ServiceType; use App\Models\DomainTld; use App\Models\HostServer; use App\Traits\NextKey; +use Carbon\Carbon; class Host extends ServiceType implements ServiceItem { @@ -34,6 +35,11 @@ class Host extends ServiceType implements ServiceItem return 'Hosting'; } + public function getServiceExpireAttribute(): Carbon + { + // TODO: Implement getServiceExpireAttribute() method. + } + public function getServiceNameAttribute(): string { return sprintf('%s.%s',strtoupper($this->domain_name),strtoupper($this->tld->name)); diff --git a/app/Models/Service/SSL.php b/app/Models/Service/SSL.php index 8d76f1a..33455ef 100644 --- a/app/Models/Service/SSL.php +++ b/app/Models/Service/SSL.php @@ -58,6 +58,11 @@ class SSL extends ServiceType implements ServiceItem } } + public function getServiceExpireAttribute(): Carbon + { + // TODO: Implement getServiceExpireAttribute() method. + } + public function getServiceNameAttribute(): string { return $this->cert diff --git a/app/Models/Service/Voip.php b/app/Models/Service/Voip.php index 72e0ca9..4ed3ad5 100644 --- a/app/Models/Service/Voip.php +++ b/app/Models/Service/Voip.php @@ -5,6 +5,7 @@ namespace App\Models\Service; use App\Interfaces\ServiceItem; use App\Models\Base\ServiceType; use App\Traits\NextKey; +use Carbon\Carbon; class Voip extends ServiceType implements ServiceItem { @@ -27,6 +28,11 @@ class Voip extends ServiceType implements ServiceItem return $this->service_address ?: 'VOIP'; } + public function getServiceExpireAttribute(): Carbon + { + // TODO: Implement getServiceExpireAttribute() method. + } + /** * Return the service number * diff --git a/app/Models/User.php b/app/Models/User.php index df0ec8f..e690175 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -74,6 +74,8 @@ class User extends Authenticatable 'customer', ]; + /* RELATIONS */ + /** * The accounts that this user manages * @@ -175,7 +177,7 @@ class User extends Authenticatable return $this->hasMany(static::class,'parent_id','id'); } - /** ATTRIBUTES **/ + /* ATTRIBUTES */ public function getActiveDisplayAttribute($value) { @@ -268,6 +270,8 @@ class User extends Authenticatable return sprintf('%s',$this->id,$this->user_id); } + /* METHODS */ + /** * Users password reset email notification * @@ -278,7 +282,7 @@ class User extends Authenticatable $this->notify((new ResetPasswordNotification($token))->onQueue('high')); } - /** SCOPES */ + /* SCOPES */ // @todo use trait public function scopeActive() @@ -323,6 +327,8 @@ class User extends Authenticatable return $query; } + /* GENERAL METHODS */ + /** * Determine if the user is an admin of the account with $id * @@ -334,8 +340,6 @@ class User extends Authenticatable return $id AND $this->isReseller() AND in_array($id,$this->all_accounts()->pluck('id')->toArray()); } - /** FUNCTIONS */ - /** * Get a list of accounts for the clients of this user * @@ -437,8 +441,8 @@ class User extends Authenticatable public function client_service_movements(): DatabaseCollection { return Service::active() + ->authorised($this) ->where('order_status','!=','ACTIVE') - ->whereIN('account_id',$this->all_accounts()->pluck('id')->unique()->toArray()) ->with(['account','product']) ->get(); } @@ -630,6 +634,14 @@ class User extends Authenticatable ->from($payment,'summary'); } + /** + * Determine what the logged in user's role is + * + Wholesaler - aka Super User + * + Reseller - services accounts on behalf of their customers + * + Customer - end user customer + * + * @return string + */ public function role() { // If I have agents and no parent, I am the wholesaler diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 39ea383..e72cd67 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -31,5 +31,9 @@ class AuthServiceProvider extends ServiceProvider Gate::define('wholesaler', function ($user) { return $user->isWholesaler(); }); + + Gate::define('reseller', function ($user) { + return $user->isReseller(); + }); } } \ No newline at end of file diff --git a/app/Traits/ScopeServiceActive.php b/app/Traits/ScopeServiceActive.php new file mode 100644 index 0000000..5350dfd --- /dev/null +++ b/app/Traits/ScopeServiceActive.php @@ -0,0 +1,25 @@ +where(function($q) { + return $q->where('ab_service.active',TRUE) + ->orWhere(function($q) { + return $q->whereNotNull('order_status') + ->whereNotIn('ab_service.order_status',Service::INACTIVE_STATUS); + }); + }); + } +} diff --git a/app/Traits/ScopeServiceUserAuthorised.php b/app/Traits/ScopeServiceUserAuthorised.php new file mode 100644 index 0000000..ad6f4ca --- /dev/null +++ b/app/Traits/ScopeServiceUserAuthorised.php @@ -0,0 +1,21 @@ +whereIN('ab_service.account_id',$uo->all_accounts()->pluck('id')->unique()->toArray()); + } +} diff --git a/database/migrations/2021_07_07_164413_rework_account.php b/database/migrations/2021_07_07_164413_rework_account.php index 4dd09ee..3c06ce4 100644 --- a/database/migrations/2021_07_07_164413_rework_account.php +++ b/database/migrations/2021_07_07_164413_rework_account.php @@ -28,6 +28,6 @@ class ReworkAccount extends Migration */ public function down() { - //abort(500,'cant go back'); + abort(500,'cant go back'); } } diff --git a/database/migrations/2021_07_09_141228_rework_domains.php b/database/migrations/2021_07_09_141228_rework_domains.php new file mode 100644 index 0000000..b7223c6 --- /dev/null +++ b/database/migrations/2021_07_09_141228_rework_domains.php @@ -0,0 +1,35 @@ +renameColumn('registrar_type','registrar_account'); + $table->unique(['domain_name','domain_tld_id']); + $table->dropColumn(['registrar_password','registrar_pending_transfer']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + abort(500,'cant go back'); + } +} diff --git a/public/css/custom.css b/public/css/custom.css index 2dcb4df..d6b52cd 100644 --- a/public/css/custom.css +++ b/public/css/custom.css @@ -3,6 +3,14 @@ hr.d-print-block { padding-bottom: 20px; } +.table:not(.table-borderless) tbody tr:last-child th, .table:not(.table-borderless) tbody tr:last-child td { - border-bottom: 2px solid #dee2e6; + border-bottom: 1px solid #dee2e6; +} + +@media print { + .table:not(.table-borderless) tbody tr:last-child th, + .table:not(.table-borderless) tbody tr:last-child td { + border-bottom: 2px solid #dee2e6; + } } \ No newline at end of file diff --git a/resources/views/theme/backend/adminlte/a/service/widgets/domain/update.blade.php b/resources/views/theme/backend/adminlte/a/service/widgets/domain/update.blade.php new file mode 100644 index 0000000..f8e1e7d --- /dev/null +++ b/resources/views/theme/backend/adminlte/a/service/widgets/domain/update.blade.php @@ -0,0 +1,172 @@ +
Service ID | +Account | +Domain | +Expires | +Registrar | +DNS Host | +Next Billed | +Price | +Term | +
---|---|---|---|---|---|---|---|---|
{{ $oo->service->sid }} | +{{ $oo->service->account->name }} | +{{ $oo->service_name }} | +{{ $oo->service_expire->format('Y-m-d') }} | +{{ $oo->registrar->name }} | +{{ $oo->registrar_ns }} | +@if ($oo->service->isBilled()) {{ $oo->service->invoice_next->format('Y-m-d') }} @else - @endif | +@if (! $oo->service->external_billing)${{ number_format($oo->service->next_invoice_items(TRUE)->sum('total'),2) }}@else - @endif | +{{ $oo->service->billing_period }} | +
REPORT
+ + + +