Enable editing of supplier products and listing services connected to them

This commit is contained in:
Deon George
2022-06-30 23:51:20 +10:00
parent fb416306e7
commit 5297ae8a62
33 changed files with 963 additions and 182 deletions

View File

@@ -2,7 +2,8 @@
namespace App\Http\Controllers;
use App\Http\Requests\SupplierAddEdit;
use Illuminate\Http\Request;
use App\Http\Requests\{SupplierAddEdit,SupplierProductAddEdit};
use App\Models\{Cost,Supplier,SupplierDetail};
class SupplierController extends Controller
@@ -67,6 +68,111 @@ class SupplierController extends Controller
return view('supplier.cost',['o'=>$o]);
}
/**
* New Product from a supplier
*
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
*/
public function product_add()
{
return view('supplier.product.addedit')
->with('o',new Supplier)
->with('oo',NULL);
}
public function product_addedit(SupplierProductAddEdit $request,Supplier $o,int $id,string $type)
{
// Quick validation
if ($type !== $request->offering_type)
abort(500,'Type and offering type do not match');
if ($o->exists && ($o->detail->id !== (int)$request->supplier_detail_id))
abort(500,sprintf('Supplier [%d] and supplier_detail_id [%d] do not match',$o->detail->id,$request->supplier_detail_id));
switch ($request->offering_type) {
case 'broadband':
$oo = Supplier\Broadband::findOrNew($id);
// @todo these are broadband requirements - get them from the broadband class.
foreach ($request->only([
'supplier_detail_id',
'product_id'.
'product_desc',
'base_cost',
'setup_cost',
'contract_term',
'metric',
'speed',
'technology',
'offpeak_start',
'offpeak_end',
'base_down_peak',
'base_up_peak',
'base_down_offpeak',
'base_up_offpeak',
'extra_down_peak',
'extra_up_peak',
'extra_down_offpeak',
'extra_up_offpeak',
]) as $key => $value)
$oo->$key = $value;
// Our boolean values
foreach ($request->only(['active','extra_shaped','extra_charged']) as $key => $value)
$oo->$key = ($value == 'on' ? 1 : 0);
break;
default:
throw new \Exception('Unknown offering type:'.$request->offering_type);
}
$oo->save();
return redirect()->back()
->with('success','Saved');
}
/**
* Edit a supplier product
*
* @param Supplier $o
* @param int $id
* @param string $type
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
*/
public function product_view(Supplier $o,int $id,string $type)
{
$oo = $o->detail->find($type,$id);
$oo->load(['products.product.services.product.type']);
return view('supplier.product.addedit')
->with('o',$o)
->with('oo',$oo);
}
/**
* Return the form for a specific product type
*
* @param Request $request
* @param string $type
* @param int|null $id
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
*/
public function product_view_type(Request $request,string $type,int $id=NULL)
{
$o = $id ? Supplier::offeringTypeClass($type)->findOrFail($id) : NULL;
if ($request->old)
$request->session()->flashInput($request->old);
if ($o)
$o->load(['products.product.services']);
return view('supplier.product.widget.'.$type)
->with('o',$id ? $o : NULL)
->withErrors($request->errors);
}
/**
* View a supplier.
*

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Rule;
use App\Models\Supplier;
class SupplierProductAddEdit extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return Auth::user()->isWholesaler();
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(Request $request)
{
// @todo these are broadband requirements - perhaps move them to the broadband class.
// @todo Enhance the validation so that extra_* values are not accepted if base_* values are not included.
return [
'id' => 'required|nullable',
'offering_type' => ['required',Rule::in(Supplier::offeringTypeKeys()->toArray())],
'supplier_detail_id' => 'required|exists:supplier_details,id',
'active' => 'sometimes|accepted',
'extra_shaped' => 'sometimes|accepted',
'extra_charged' => 'sometimes|accepted',
'product_id' => 'required|string|min:2',
'product_desc' => 'required|string|min:2',
'base_cost' => 'required|numeric|min:.01',
'setup_cost' => 'nullable|numeric',
'contract_term' => 'nullable|numeric|min:1',
'metric' => 'nullable|numeric|min:1',
'speed' => 'nullable|string|max:64',
'technology' => 'nullable|string|max:255',
'offpeak_start' => 'nullable|date_format:H:i',
'offpeak_end' => 'nullable|date_format:H:i',
'base_down_peak' => 'nullable|numeric',
'base_up_peak' => 'nullable|numeric',
'base_down_offpeak' => 'nullable|numeric',
'base_up_offpeak' => 'nullable|numeric',
'extra_down_peak' => 'nullable|numeric',
'extra_up_peak' => 'nullable|numeric',
'extra_down_offpeak' => 'nullable|numeric',
'extra_up_offpeak' => 'nullable|numeric',
];
}
}

View File

@@ -16,9 +16,9 @@ interface SupplierItem
/**
* Available products created from this supplier offering
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function types();
public function products();
/* ATTRIBUTES */

View File

@@ -183,11 +183,10 @@ class Product extends Model implements IDs
* Return the type of service is provided. eg: Broadband, Phone.
*
* @return string
* @todo Does type need to be a mandatory attribute on a model - then we can remove this condition
*/
public function getCategoryAttribute(): string
{
return $this->type ? $this->type->getCategoryAttribute() : 'generic';
return $this->supplied->getCategoryAttribute();
}
/**
@@ -195,11 +194,10 @@ class Product extends Model implements IDs
* other logic of these types.
*
* @return string
* @todo Does type need to be a mandatory attribute on a model - then we can remove this condition
*/
public function getCategoryNameAttribute(): string
{
return $this->type ? $this->type->getCategoryNameAttribute() : 'Generic';
return $this->supplied->getCategoryNameAttribute();
}
/**

View File

@@ -15,8 +15,6 @@ final class Broadband extends Type implements ProductItem
protected $table = 'product_broadband';
protected const category_name = 'Broadband';
// Information required during the order process
protected array $order_attributes = [
'options.address'=>[

View File

@@ -12,8 +12,6 @@ final class Domain extends Type implements ProductItem
{
protected $table = 'product_domain';
protected const category_name = 'Domain Name';
// The model that is referenced when this product is ordered
protected string $order_model = ServiceDomain::class;

View File

@@ -12,8 +12,6 @@ final class Email extends Type implements ProductItem
{
protected $table = 'product_email';
protected const category_name = 'Email Hosting';
// The model that is referenced when this product is ordered
protected string $order_model = ServiceEmail::class;

View File

@@ -12,8 +12,6 @@ final class Generic extends Type implements ProductItem
{
protected $table = 'product_generic';
protected const category_name = 'Generic';
// The model that is referenced when this product is ordered
protected string $order_model = ServiceGeneric::class;

View File

@@ -12,8 +12,6 @@ final class Host extends Type implements ProductItem
{
protected $table = 'product_host';
protected const category_name = 'Web Hosting';
// The model that is referenced when this product is ordered
protected string $order_model = ServiceHost::class;

View File

@@ -12,8 +12,6 @@ final class Phone extends Type implements ProductItem
{
protected $table = 'product_phone';
protected const category_name = 'Telephone';
protected array $order_attributes = [
'options.phonenumber'=>[
'request'=>'options.phonenumber',

View File

@@ -12,8 +12,6 @@ final class SSL extends Type implements ProductItem
{
protected $table = 'product_ssl';
protected const category_name = 'SSL Certificate';
// The model that is referenced when this product is ordered
protected string $order_model = ServiceSSL::class;

View File

@@ -32,9 +32,11 @@ abstract class Type extends Model
* other logic of these types.
*
* @return string
* @deprecated - can this be replaced with product->supplied->category?
*/
final public function getCategoryAttribute(): string
{
abort(500,'use product->supplied->category_name');
return strtolower((new \ReflectionClass($this))->getShortName());
}
@@ -42,9 +44,11 @@ abstract class Type extends Model
* Return a friendly name for this product, used for display
*
* @return string
* @deprecated - can this be replaced with product->supplied->category_name
*/
final public function getCategoryNameAttribute(): string
{
abort(500,'use product->supplied->category_name');
return static::category_name;
}
}

View File

@@ -8,57 +8,93 @@ use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Leenooks\Traits\ScopeActive;
use App\Models\Supplier\{Broadband,Domain,Email,Ethernet,Generic,Host,HSPA,Phone,SSL};
use App\Models\Supplier\{Broadband,Domain,Email,Ethernet,Generic,Host,HSPA,Phone,SSL,Type};
class Supplier extends Model
{
/**
* The offerings types we provide
*/
private const offering_types = [
'broadband' => Broadband::class,
'hspa' => HSPA::class,
'ethernet' => Ethernet::class,
'domainname' => Domain::class,
'email' => Email::class,
'generic' => Generic::class,
'hosting' => Host::class,
'phone' => Phone::class,
'ssl' => SSL::class,
];
use ScopeActive;
public $timestamps = FALSE;
/* STATIC METHODS */
/**
* The offerings we provide
* @todo Use the product/* category instead of this const. The assumption is the supplier/* type is the same as the product/* type.
* @deprecated - use the product/* category instead.
* Return the offerings that this supplier provides
*
* @param Supplier|null $so
* @return Collection
*/
public const offering_types = [
'broadband' => [
'name' => 'Broadband',
'class' => Broadband::class,
],
'hspa' => [
'name' => 'Mobile Broadband',
'class' => HSPA::class,
],
'ethernet' => [
'name' => 'Ethernet Broadband',
'class' => Ethernet::class,
],
'domainname' => [
'name' => 'Domain Name',
'class' => Domain::class,
],
'email' => [
'name' => 'Email Hosting',
'class' => Email::class,
],
'generic' => [
'name' => 'Generic',
'class' => Generic::class,
],
'hosting' => [
'name' => 'Hosting',
'class' => Host::class,
],
'phone' => [
'name' => 'Phone',
'class' => Phone::class,
],
'ssl' => [
'name' => 'SSL',
'class' => SSL::class,
],
];
public static function offeringTypes(self $so=NULL): Collection
{
$result = collect();
foreach (self::offering_types as $type) {
$class = new $type;
if ($so) {
// If we have a connections configuration for that supplier, then build the child relationships
if (Arr::get($so->detail->connections,$class->category)) {
$result->put($class->category,(object)[
'type' => $class->category_name,
'items' => $class->where('supplier_detail_id',$so->detail->id),
]);
continue;
}
// Even if we dont have any connections, see if we have any products defined
$o = new $class;
$o->where('supplier_detail_id',$so->detail->id);
if ($o->count())
$result->put($class->category,(object)[
'type' => $class->category_name,
'items' => $class->where('supplier_detail_id',$so->detail->id),
]);
} else {
$result->put($class->category_name,$class);
}
}
return $result;
}
/**
* Return a new model object for the offering type
*
* @param string $type
* @return Type
*/
public static function offeringTypeClass(string $type): Type
{
return ($class=collect(self::offering_types)->get($type)) ? new $class : new Generic;
}
/**
* Return our supported offering type keys
*
* @return Collection
*/
public static function offeringTypeKeys(): Collection
{
return collect(self::offering_types)->keys();
}
/* RELATIONS */
@@ -73,43 +109,6 @@ class Supplier extends Model
/* METHODS */
/**
* Return the offerings that this supplier provides
*
* @return void
*/
public function offeringTypes(): Collection
{
$result = collect();
// See if we have any configurations
foreach (self::offering_types as $key => $type) {
if (! ($class=Arr::get($type,'class')))
continue;
if (Arr::get($this->detail->connections,$key)) {
$result->put($key,(object)[
'type' => Arr::get($type,'name'),
'items' => (new $class)->where('supplier_detail_id',$this->detail->id),
]);
continue;
}
// See if we have any products defined
$o = new $class;
$o->where('supplier_detail_id',$this->detail->id);
if ($o->count())
$result->put($key,(object)[
'type' => Arr::get($type,'name'),
'items' => (new $class)->where('supplier_detail_id',$this->detail->id),
]);
}
return $result;
}
/**
* Return the traffic records, that were not matched to a service.
*

View File

@@ -10,6 +10,8 @@ use App\Models\Product\Broadband as ProductBroadband;
class Broadband extends Type implements SupplierItem
{
protected const category_name = 'Broadband';
protected $casts = [
'offpeak_start' => 'datetime:H:i',
'offpeak_end' => 'datetime:H:i',
@@ -35,16 +37,16 @@ class Broadband extends Type implements SupplierItem
/* INTERFACES */
public function types()
{
return $this->belongsToMany(ProductBroadband::class,$this->table,'id','id','id','supplier_item_id');
}
public function getBillingIntervalAttribute(): int
{
return 1; // Monthly
}
public function products()
{
return $this->hasMany(ProductBroadband::class,'supplier_item_id','id');
}
/* METHODS */
/**
@@ -156,14 +158,4 @@ class Broadband extends Type implements SupplierItem
return $result;
}
/**
* Return the Broadband Speed
*
* @return string
*/
public function speed(): string
{
return $this->speed;
}
}

View File

@@ -8,6 +8,8 @@ use App\Models\TLD;
final class Domain extends Type implements SupplierItem
{
protected const category_name = 'Domain Name';
protected $table = 'supplier_domain';
/* INTERFACES */
@@ -22,9 +24,9 @@ final class Domain extends Type implements SupplierItem
return sprintf('%s: %s',$this->product_id,$this->tld->name);
}
public function types()
public function products()
{
return $this->belongsToMany(ProductDomain::class,$this->table,'id','id','id','supplier_item_id');
return $this->hasMany(ProductDomain::class,'supplier_item_id','id');
}
/* RELATIONS */

View File

@@ -7,6 +7,8 @@ use App\Models\Product\Email as ProductEmail;
final class Email extends Type implements SupplierItem
{
protected const category_name = 'Email Hosting';
protected $table = 'supplier_email';
/* INTERFACES */
@@ -16,8 +18,8 @@ final class Email extends Type implements SupplierItem
return 4; // Yearly
}
public function types()
public function products()
{
return $this->belongsToMany(ProductEmail::class,$this->table,'id','id','id','supplier_item_id');
return $this->hasMany(ProductEmail::class,$this->table,'supplier_item_id','id');
}
}

View File

@@ -4,4 +4,5 @@ namespace App\Models\Supplier;
class Ethernet extends Broadband
{
protected const category_name = 'Broadband Ethernet';
}

View File

@@ -7,17 +7,19 @@ use App\Models\Product\Generic as ProductGeneric;
final class Generic extends Type implements SupplierItem
{
protected const category_name = 'Generic';
protected $table = 'supplier_generic';
/* INTERFACES */
public function types()
{
return $this->belongsToMany(ProductGeneric::class,$this->table,'id','id','id','supplier_item_id');
}
public function getBillingIntervalAttribute(): int
{
return 1; // Monthly
}
public function products()
{
return $this->hasMany(ProductGeneric::class,'supplier_item_id','id');
}
}

View File

@@ -4,4 +4,5 @@ namespace App\Models\Supplier;
class HSPA extends Broadband
{
protected const category_name = 'Mobile Broadband';
}

View File

@@ -7,17 +7,19 @@ use App\Models\Product\Host as ProductHost;
final class Host extends Type implements SupplierItem
{
protected const category_name = 'Web Hosting';
protected $table = 'supplier_host';
/* INTERFACES */
public function types()
{
return $this->belongsToMany(ProductHost::class,$this->table,'id','id','id','supplier_item_id');
}
public function getBillingIntervalAttribute(): int
{
return 4; // Yearly
}
public function products()
{
return $this->belongsToMany(ProductHost::class,'supplier_item_id','id');
}
}

View File

@@ -7,17 +7,19 @@ use App\Models\Product\Phone as ProductVoip;
final class Phone extends Type implements SupplierItem
{
protected const category_name = 'Telephone';
protected $table = 'supplier_phone';
/* INTERFACES */
public function types()
{
return $this->belongsToMany(ProductVoip::class,$this->table,'id','id','id','supplier_item_id');
}
public function getBillingIntervalAttribute(): int
{
return 1; // Monthly
}
public function products()
{
return $this->hasMany(ProductVoip::class,'supplier_item_id','id');
}
}

View File

@@ -7,17 +7,19 @@ use App\Models\Product\SSL as ProductSSL;
final class SSL extends Type implements SupplierItem
{
protected const category_name = 'SSL Certificate';
protected $table = 'supplier_ssl';
/* INTERFACES */
public function types()
{
return $this->belongsToMany(ProductSSL::class,$this->table,'id','id','id','supplier_item_id');
}
public function getBillingIntervalAttribute(): int
{
return 4; // Yearly
}
public function products()
{
return $this->belongsToMany(ProductSSL::class,'supplier_item_id','id');
}
}

View File

@@ -27,6 +27,27 @@ abstract class Type extends Model
return Tax::tax_calc($this->attributes['base_cost'],config('site')->taxes);
}
/**
* This will return the category of the product (eg: domain, hosting, etc) which is the basis for all
* other logic of these types.
*
* @return string
*/
final public function getCategoryAttribute(): string
{
return strtolower((new \ReflectionClass($this))->getShortName());
}
/**
* Return a friendly name for this product, used for display
*
* @return string
*/
final public function getCategoryNameAttribute(): string
{
return static::category_name;
}
/**
* This contract term is the highest of
* + The defined contract_term

View File

@@ -3,7 +3,9 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use App\Models\Supplier\{Broadband,Generic,Phone,Type};
use App\Traits\SiteID;
class SupplierDetail extends Model
@@ -14,8 +16,50 @@ class SupplierDetail extends Model
/* RELATIONS */
public function broadbands()
{
return $this->hasMany(Broadband::class);
}
public function generics()
{
return $this->hasMany(Generic::class);
}
public function phones()
{
return $this->hasMany(Phone::class);
}
public function supplier()
{
return $this->belongsTo(Supplier::class);
}
/* METHODS */
/**
* Find a supplier product of a particular type
*
* @param string $type
* @param int $id
* @return Type
* @throws \Exception
*/
public function find(string $type,int $id): Type
{
switch ($type) {
case 'broadband':
$item = $this->broadbands->where('id',$id);
break;
default:
throw new \Exception('Unknown type: '.$type);
}
if ($item->count() !== 1)
throw new ModelNotFoundException(sprintf('Unknown Model of type [%s] with id [%d]',$type,$id));
return $item->pop();
}
}