diff --git a/app/Http/Controllers/ProductController.php b/app/Http/Controllers/ProductController.php new file mode 100644 index 0000000..8eb09c2 --- /dev/null +++ b/app/Http/Controllers/ProductController.php @@ -0,0 +1,100 @@ +type) { + case 'App\Models\Product\Broadband': + return Product\Broadband::select(['id','supplier_broadband_id']) + ->with(['supplied.supplier_detail.supplier']) + ->get() + ->map(function($item) { return ['id'=>$item->id,'name'=>sprintf('%s: %s',$item->supplied->supplier_detail->supplier->name,$item->supplied->name)]; }) + ->sortBy('name') + ->values(); + + case 'App\Models\Product\Email': + return Product\Email::select(['id','supplier_email_id']) + ->with(['supplied.supplier_detail.supplier']) + ->get() + ->map(function($item) { return ['id'=>$item->id,'name'=>sprintf('%s: %s',$item->supplied->supplier_detail->supplier->name,$item->supplied->name)]; }) + ->sortBy('name') + ->values(); + + default: + throw new \Exception('Unknown type: '.$request->type); + } + } + + /** + * Update a suppliers details + * + * @param Request $request + * @param Product $o + * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|\Illuminate\Http\RedirectResponse + */ + public function details(Request $request,Product $o) + { + if ($request->post()) { + $validation = $request->validate([ + 'description.name' => 'required|string|min:2|max:255', + 'active' => 'sometimes|accepted', + 'model' => 'sometimes|string', // @todo Check that it is a valid model type + 'model_id' => 'sometimes|int', // @todo Check that it is a valid model type + ]); + + foreach (collect($validation)->except('description') as $key => $item) + $o->{$key} = $item; + + $o->active = (bool)$request->active; + + try { + $o->save(); + } catch (\Exception $e) { + return redirect()->back()->withErrors($e->getMessage())->withInput(); + } + + $o->load(['description']); + $oo = $o->description ?: new ProductTranslate; + + foreach (collect($validation)->get('description',[]) as $key => $item) + $oo->{$key} = $item; + + $o->description()->save($oo); + + return redirect()->back() + ->with('success','Product saved'); + } + + if (! $o->exists && $request->name) + $o = Product::where('name',$request->name)->firstOrNew(); + + return view('a.product.details') + ->with('o',$o); + } + + /** + * Manage products for a site + * + * @note This method is protected by the routes + * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View + */ + public function home() + { + return view('a.product.home'); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/ServiceController.php b/app/Http/Controllers/ServiceController.php index 7b563dc..7d44e40 100644 --- a/app/Http/Controllers/ServiceController.php +++ b/app/Http/Controllers/ServiceController.php @@ -255,6 +255,20 @@ class ServiceController extends Controller ->with('o',$o); } + public function email_list(): View + { + // @todo Need to add the with path when calculating next_billed and price + $o = Service\Email::serviceActive() + ->serviceUserAuthorised(Auth::user()) + ->select('service_emails.*') + ->join('ab_service',['ab_service.id'=>'service_emails.service_id']) + ->with(['service.account','service.product.type.supplied.supplier_detail.supplier','tld']) + ->get(); + + return view('r.service.email.list') + ->with('o',$o); + } + /** * Update details about a service * diff --git a/app/Models/Base/ServiceType.php b/app/Models/Base/ServiceType.php index 2d6310f..9fa7636 100644 --- a/app/Models/Base/ServiceType.php +++ b/app/Models/Base/ServiceType.php @@ -9,7 +9,6 @@ use App\Models\Service; abstract class ServiceType extends Model { public $timestamps = FALSE; - public $dateFormat = 'U'; /** * @NOTE: The service_id column could be discarded, if the id column=service_id diff --git a/app/Models/Product.php b/app/Models/Product.php index 7414a24..11d6bea 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -2,16 +2,18 @@ namespace App\Models; +use Illuminate\Container\Container; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Auth; use Illuminate\Http\Request; +use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Log; use Leenooks\Traits\ScopeActive; -use App\Interfaces\IDs; +use App\Interfaces\{IDs,ProductItem}; use App\Traits\{ProductDetails,SiteID}; /** @@ -23,6 +25,7 @@ use App\Traits\{ProductDetails,SiteID}; * * Attributes for products: * + lid : Local ID for product (part number) + * + sid : System ID for product (part number) * + supplied : Supplier product provided for this offering * + supplier : Supplier for this offering * + name : Brief Name for our product @@ -36,6 +39,7 @@ use App\Traits\{ProductDetails,SiteID}; * + base_charge_taxable : Default billing amount including taxes * + min_charge : Minimum cost taking into account billing interval and setup costs * + min_charge_taxable : Minimum cost taking into account billing interval and setup costs including taxes + * + type : Returns the underlying product object, representing the type of product * * Attributes for product types (type - Product/*) * + name : Short Name for our Product @@ -66,6 +70,8 @@ class Product extends Model implements IDs 'pricing'=>'collection', ]; + protected $with = ['description']; + /* RELATIONS */ /** @@ -145,7 +151,7 @@ class Product extends Model implements IDs */ public function getBaseCostAttribute(): float { - return round($this->type->supplied->base_cost*Invoice::billing_change($this->type->supplied->getBillingIntervalAttribute(),$this->getBillingIntervalAttribute()) ?: 0,2); + return round($this->getSuppliedAttribute()->base_cost*Invoice::billing_change($this->getSuppliedAttribute()->getBillingIntervalAttribute(),$this->getBillingIntervalAttribute()) ?: 0,2); } /** @@ -167,7 +173,7 @@ class Product extends Model implements IDs */ public function getBillingIntervalAttribute(): int { - return max($this->price_recur_default,$this->type->supplied->getBillingIntervalAttribute()); + return max($this->price_recur_default,$this->getSuppliedAttribute()->getBillingIntervalAttribute()); } /** @@ -239,10 +245,31 @@ class Product extends Model implements IDs * Get our product type * * @return string + * @todo is the test of type and type->supplied necessary? */ public function getProductTypeAttribute(): string { - return ($this->type && $this->type->supplied) ? $this->type->supplied->getTypeAttribute() : 'Unknown'; + return ($this->type && $this->type->supplied) ? $this->getSuppliedAttribute()->getTypeAttribute() : 'Unknown'; + } + + /** + * Suppliers + * + * @return Model + */ + public function getSupplierAttribute(): Model + { + return $this->getSuppliedAttribute()->supplier_detail->supplier; + } + + /** + * Suppliers product + * + * @return Model + */ + public function getSuppliedAttribute(): Model + { + return $this->type->supplied; } /** @@ -277,7 +304,7 @@ class Product extends Model implements IDs */ public function getSetupCostAttribute(): float { - return $this->type->supplied->setup_cost ?: 0; + return $this->getSuppliedAttribute()->setup_cost ?: 0; } /** @@ -294,13 +321,33 @@ class Product extends Model implements IDs /* METHODS */ /** - * Return if this product captures usage data + * Return a list of available product types * - * @return bool + * @return Collection */ - public function hasUsage(): bool + function availableTypes(): Collection { - return $this->type->hasUsage(); + $models = collect(File::allFiles(app_path())) + ->map(function ($item) { + $path = $item->getRelativePathName(); + $class = sprintf('%s%s', + Container::getInstance()->getNamespace(), + strtr(substr($path, 0, strrpos($path, '.')), '/', '\\')); + + return $class; + }) + ->filter(function ($class) { + $valid = FALSE; + + if (class_exists($class)) { + $reflection = new \ReflectionClass($class); + $valid = $reflection->isSubclassOf(ProductItem::class) && (! $reflection->isAbstract()); + } + + return $valid; + }); + + return $models->values(); } /** @@ -347,6 +394,16 @@ class Product extends Model implements IDs return round($price,2); } + /** + * Return if this product captures usage data + * + * @return bool + */ + public function hasUsage(): bool + { + return $this->type->hasUsage(); + } + /** * When receiving an order, validate that we have all the required information for the product type * diff --git a/app/Models/Product/Broadband.php b/app/Models/Product/Broadband.php index 75be01f..b9ec9d5 100644 --- a/app/Models/Product/Broadband.php +++ b/app/Models/Product/Broadband.php @@ -3,6 +3,7 @@ namespace App\Models\Product; use Illuminate\Support\Collection; +use Leenooks\Traits\ScopeActive; use App\Interfaces\ProductItem; use App\Models\Supplier; @@ -11,6 +12,8 @@ use App\Models\Supplier\Broadband as SupplierBroadband; final class Broadband extends Type implements ProductItem { + use ScopeActive; + protected $table = 'product_broadband'; // Information required during the order process @@ -44,17 +47,6 @@ final class Broadband extends Type implements ProductItem return $this->hasOne(SupplierBroadband::class,'id','supplier_broadband_id'); } - /** - * The supplier - * - * @return \Illuminate\Database\Eloquent\Relations\HasOneThrough - */ - // @todo To check - public function supplier() - { - return $this->hasOneThrough(Supplier::class,SupplierBroadband::class,'id','id','adsl_supplier_plan_id','supplier_id'); - } - /* INTERFACES */ /** diff --git a/app/Models/Product/Email.php b/app/Models/Product/Email.php new file mode 100644 index 0000000..8198c7f --- /dev/null +++ b/app/Models/Product/Email.php @@ -0,0 +1,69 @@ +hasOne(SupplierEmail::class,'id','supplier_email_id'); + } + + /* INTERFACES */ + + public function allowance(): Collection + { + // N/A + return collect(); + } + + public function allowance_string(): string + { + // N/A + return ''; + } + + public function getContractTermAttribute(): int + { + return 12; + } + + public function getCostAttribute(): float + { + // N/A + return 0; + } + + public function getSupplierAttribute() + { + return ''; + } + + public function getTypeAttribute() + { + return 'Domain Name'; + } + + public function hasUsage(): bool + { + return FALSE; + } +} \ No newline at end of file diff --git a/app/Models/ProductTranslate.php b/app/Models/ProductTranslate.php index 3f629b0..83da067 100644 --- a/app/Models/ProductTranslate.php +++ b/app/Models/ProductTranslate.php @@ -8,6 +8,8 @@ class ProductTranslate extends Model { protected $table = 'ab_product_translate'; + public $timestamps = FALSE; + public function getDescriptionFullAttribute($value) { return unserialize($value); diff --git a/app/Models/Service.php b/app/Models/Service.php index 5779f4f..7516537 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -27,6 +27,7 @@ use App\Interfaces\IDs; * + additional_cost : Pending additional charges for this service (excluding setup) * + billing_cost : Charge for this service each invoice period * + billing_interval : The period that this service is billed for by default + * + billing_interval_string : The period that this service is billed for by default as a name * + name : Service short name with service address * + name_short : Service Product short name, eg: phone number, domain name, certificate CN * + name_detail : Service Detail, eg: service_address @@ -880,11 +881,11 @@ class Service extends Model implements IDs * This is used for view specific details * * @return string + * @todo I think this can be removed - and dynamically determined */ public function getSTypeAttribute(): string { switch($this->product->model) { - case 'App\Models\Product\Broadband': return 'broadband'; default: return $this->type->type; } } diff --git a/app/Models/Service/AdslTraffic.php b/app/Models/Service/AdslTraffic.php index 34814da..697237d 100644 --- a/app/Models/Service/AdslTraffic.php +++ b/app/Models/Service/AdslTraffic.php @@ -10,6 +10,7 @@ class AdslTraffic extends Model protected $table = 'ab_service__adsl_traffic'; public $timestamps = FALSE; protected $dates = ['date']; + public $dateFormat = 'U'; private $traffic_end = 14; public function broadband() diff --git a/app/Models/Service/Broadband.php b/app/Models/Service/Broadband.php index 06cc33b..ad0288e 100644 --- a/app/Models/Service/Broadband.php +++ b/app/Models/Service/Broadband.php @@ -22,6 +22,7 @@ class Broadband extends ServiceType implements ServiceItem,ServiceUsage 'service_connect_date', 'service_contract_date' ]; + public $dateFormat = 'U'; protected $table = 'ab_service__adsl'; /* RELATIONS */ diff --git a/app/Models/Service/Domain.php b/app/Models/Service/Domain.php index 339c00e..c99ae99 100644 --- a/app/Models/Service/Domain.php +++ b/app/Models/Service/Domain.php @@ -27,46 +27,11 @@ class Domain extends ServiceType implements ServiceItem protected $dates = [ 'domain_expire', ]; + public $dateFormat = 'U'; 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'); - } - - public function tld() - { - 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 */ + /* INTERFACES */ public function getServiceDescriptionAttribute(): string { @@ -93,4 +58,41 @@ class Domain extends ServiceType implements ServiceItem { return $this->domain_expire->isFuture(); } + + /* RELATIONS */ + + public function account() + { + return $this->hasOneThrough(Account::class,Service::class); + } + + public function registrar() + { + return $this->belongsTo(DomainRegistrar::class,'domain_registrar_id'); + } + public function tld() + { + 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 */ } \ No newline at end of file diff --git a/app/Models/Service/Email.php b/app/Models/Service/Email.php new file mode 100644 index 0000000..abfb661 --- /dev/null +++ b/app/Models/Service/Email.php @@ -0,0 +1,65 @@ +expire_at ?: $this->service->next_invoice; + } + + /** + * The name of the domain with its TLD + * + * @return string + * // @todo + */ + public function getServiceNameAttribute(): string + { + return strtoupper(sprintf('%s.%s',$this->domain_name,$this->tld->name)); + } + + public function inContract(): bool + { + return $this->expire_at && $this->expire_at->isFuture(); + } + + /* RELATIONS */ + + public function tld() + { + return $this->belongsTo(TLD::class); + } +} \ No newline at end of file diff --git a/app/Models/Service/Host.php b/app/Models/Service/Host.php index 9594857..0aacb5e 100644 --- a/app/Models/Service/Host.php +++ b/app/Models/Service/Host.php @@ -17,6 +17,7 @@ class Host extends ServiceType implements ServiceItem protected $dates = [ 'host_expire', ]; + public $dateFormat = 'U'; protected $table = 'ab_service__hosting'; public function provider() diff --git a/app/Models/Service/Voip.php b/app/Models/Service/Voip.php index b26b5c6..1aef1b4 100644 --- a/app/Models/Service/Voip.php +++ b/app/Models/Service/Voip.php @@ -16,6 +16,7 @@ class Voip extends ServiceType implements ServiceItem 'service_connect_date', 'service_contract_date', ]; + public $dateFormat = 'U'; protected $table = 'ab_service__voip'; /* SCOPES */ diff --git a/app/Models/Supplier.php b/app/Models/Supplier.php index ce0fefa..4cbdbe4 100644 --- a/app/Models/Supplier.php +++ b/app/Models/Supplier.php @@ -7,7 +7,7 @@ use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Leenooks\Traits\ScopeActive; -use App\Models\Supplier\{Broadband,Domain,Ethernet,Generic,Host,HSPA,Voip}; +use App\Models\Supplier\{Broadband,Domain,Email,Ethernet,Generic,Host,HSPA,Voip}; class Supplier extends Model { @@ -33,6 +33,10 @@ class Supplier extends Model 'name' => 'Domain Name', 'class' => Domain::class, ], + 'email' => [ + 'name' => 'Email Hosting', + 'class' => Email::class, + ], 'generic' => [ 'name' => 'Generic', 'class' => Generic::class, diff --git a/app/Models/Supplier/Domain.php b/app/Models/Supplier/Domain.php index a80ce0f..ab85783 100644 --- a/app/Models/Supplier/Domain.php +++ b/app/Models/Supplier/Domain.php @@ -12,11 +12,6 @@ final class Domain extends Type implements SupplierItem /* INTERFACES */ - public function types() - { - return $this->belongsToMany(ProductDomain::class,$this->table,'id','id','id',$this->table.'_id'); - } - public function getBillingIntervalAttribute(): int { return 4; // Yearly @@ -27,6 +22,11 @@ final class Domain extends Type implements SupplierItem return sprintf('%s: %s',$this->product_id,$this->tld->name); } + public function types() + { + return $this->belongsToMany(ProductDomain::class,$this->table,'id','id','id',$this->table.'_id'); + } + /* RELATIONS */ public function tld() diff --git a/app/Models/Supplier/Email.php b/app/Models/Supplier/Email.php new file mode 100644 index 0000000..6602f5c --- /dev/null +++ b/app/Models/Supplier/Email.php @@ -0,0 +1,23 @@ +belongsToMany(ProductEmail::class,$this->table,'id','id','id',$this->table.'_id'); + } +} \ No newline at end of file diff --git a/database/migrations/2022_04_02_092021_email_supplier.php b/database/migrations/2022_04_02_092021_email_supplier.php new file mode 100644 index 0000000..8850d13 --- /dev/null +++ b/database/migrations/2022_04_02_092021_email_supplier.php @@ -0,0 +1,106 @@ +integer('id',TRUE)->unsigned(); + $table->timestamps(); + $table->integer('site_id')->unsigned(); + $table->boolean('active'); + $table->integer('supplier_detail_id')->unsigned(); + $table->string('product_id'); + $table->string('product_desc')->nullable(); + $table->float('base_cost'); + $table->float('setup_cost')->nullable(); + $table->integer('contract_term')->nullable(); + + $table->foreign(['supplier_detail_id'])->references(['id'])->on('supplier_details'); + $table->foreign(['site_id'])->references(['id'])->on('sites'); + }); + + Schema::create('product_email', function (Blueprint $table) { + $table->integer('id',TRUE)->unsigned(); + $table->timestamps(); + $table->integer('site_id')->unsigned(); + $table->boolean('active'); + $table->integer('supplier_email_id')->unsigned(); + $table->string('product_id'); + $table->string('product_desc')->nullable(); + $table->float('base_cost'); + $table->float('setup_cost')->nullable(); + $table->integer('contract_term')->nullable(); + + $table->foreign(['supplier_email_id'])->references(['id'])->on('supplier_email'); + $table->foreign(['site_id'])->references(['id'])->on('sites'); + }); + + Schema::create('service_emails', function (Blueprint $table) { + $table->integer('id',TRUE)->unsigned(); + $table->integer('site_id')->unsigned(); + $table->integer('service_id')->unsigned(); + + $table->integer('tld_id')->unsigned(); + $table->string('domain_name',128); + $table->date('expire_at')->nullable(); + $table->string('admin_url')->nullable(); + $table->string('admin_user')->nullable(); + $table->string('admin_pass')->nullable(); + $table->integer('accounts')->nullable(); + + $table->foreign(['site_id'])->references(['id'])->on('sites'); + $table->foreign(['service_id'])->references(['id'])->on('ab_service'); + $table->foreign(['tld_id'])->references(['id'])->on('tlds'); + }); + + // Migrate email hosting from hosting table to service_email + // Setup Domains + foreach (\Illuminate\Support\Facades\DB::select("SELECT * FROM ab_service__hosting WHERE host_type='email'") as $o) { + $oo = new \App\Models\Service\Email; + + foreach (['site_id','service_id'] as $item) + $oo->{$item} = $o->{$item}; + + $oo->tld_id = $o->domain_tld_id; + $oo->domain_name = strtolower($o->domain_name); + $oo->admin_user = strtolower($o->host_username); + $oo->admin_pass = $o->host_password; + $oo->expire_at = \Carbon\Carbon::createFromTimestamp($o->host_expire); + $oo->save(); + + \App\Models\Service::where('id',$o->service_id)->update(['model'=>get_class($oo)]); + }; + + \Illuminate\Support\Facades\DB::select("DELETE FROM ab_service__hosting WHERE host_type='email'"); + + // insert into suppliers value (null,1,'Google',null,null,null,null,null); + // insert into supplier_details values (null,now(),now(),null,null,null,null,14,1,null); + // insert into supplier_email values (null,now(),now(),1,1,13,'Legacy Email','Legacy Email',0,0,0); + // insert into product_email values (null,now(),now(),1,1,1,'Email Hosting',null,150,0,12); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + /* + Schema::dropIfExists('product_email'); + Schema::dropIfExists('supplier_email'); + Schema::dropIfExists('service_emails'); + */ + abort(500,'cant go back'); + } +} diff --git a/resources/views/theme/backend/adminlte/a/product/details.blade.php b/resources/views/theme/backend/adminlte/a/product/details.blade.php new file mode 100644 index 0000000..eee57a3 --- /dev/null +++ b/resources/views/theme/backend/adminlte/a/product/details.blade.php @@ -0,0 +1,61 @@ +@extends('adminlte::layouts.app') + +@section('htmlheader_title') + {{ $o->name ?: 'New Product' }} +@endsection +@section('page_title') + {{ $o->name ?: 'New Product' }} +@endsection + +@section('contentheader_title') + {{ $o->name ?: 'New Product' }} +@endsection +@section('contentheader_description') +@endsection + +@section('main-content') +