name_short) * + name_detail : Details of our product (description.description_short => name_detail) * + description : Product description (description.description_full => description_full) * + billing_interval : Default Billing Interval * + billing_interval_string: Default Billing Interval in human-readable form * + setup_charge : Charge to setup this product * + setup_charge_taxable : Charge to setup this product including taxes * + base_charge : Default billing amount * + base_charge_taxable : Default billing amount including taxes * + min_charge : Minimum charge taking into account billing interval and setup charges * + min_charge_taxable : Minimum charge taking into account billing interval and setup charges including taxes * * Attributes for product types (type - Product/*) * + name : Short Name for our Product * + name_long : Long Name for our Product * + description : Description of offering (Broadband=speed) * * Attributes for supplier's offerings (type->supplied - Supplier/*) * + name : Short Name for suppliers offering * + name_long : Long Name for suppliers offering * + description : Description of offering (Broadband=speed) * * Product Pricing self::pricing is an array of: * [ * timeperiod => [ * show => true|false (show this time period to the user for ordering) * group => [ pricing/setup ] * ] * ] * * @todo doesnt appear that price_type is used - but could be used to have different offering types billed differently * @package App\Models */ class Product extends Model implements IDs { use Compoships,HasFactory,SiteID,ProductDetails,ScopeActive; protected $casts = [ 'pricing'=>'collection', ]; /* STATIC */ /** * Return a list of available product types * * @return Collection */ public static function availableTypes(): Collection { $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(); } /* RELATIONS */ /** * Which services are configured with this product * * @return \Illuminate\Database\Eloquent\Relations\HasMany */ public function services() { return $this->hasMany(Service::class); } /** * Get the product name in the users language, and if the user isnt logged in, the sites language * * @return \Illuminate\Database\Eloquent\Relations\HasOne */ public function translate() { return $this->hasOne(ProductTranslate::class) ->where('language_id',(Auth::user() && Auth::user()->language_id) ? Auth::user()->language_id : config('site')->language_id); } /** * Return a child model with details of the service * This will return a product/* model. * * @return \Illuminate\Database\Eloquent\Relations\MorphTo */ public function type() { return $this->morphTo(null,'model','model_id'); } /* INTERFACES */ public function getLIDAttribute(): string { return sprintf('%04s',$this->id); } public function getSIDAttribute(): string { return sprintf('%02s-%s',$this->site_id,$this->getLIDattribute()); } /* ATTRIBUTES */ /** * The amount we invoice each time period for this service * * @param int|NULL $timeperiod * @param Group|NULL $go * @return float */ public function getBaseChargeAttribute(int $timeperiod=NULL,Group $go=NULL): float { return $this->getCharge('base',$timeperiod,$go); } /** * The amount we invoice each time period for this service, including taxes * * @param int|null $timeperiod * @param Group|null $go * @param Collection|NULL $taxes * @return float */ public function getBaseChargeTaxableAttribute(int $timeperiod=NULL,Group $go=NULL,Collection $taxes=NULL): float { return Tax::tax_calc($this->getBaseChargeAttribute($timeperiod,$go),$taxes ?: config('site')->taxes); } /** * The base cost of this product at the appropriate billing interval * * @return float */ public function getBaseCostAttribute(): float { return round($this->getSuppliedAttribute()->base_cost*Invoice::billing_change($this->getSuppliedAttribute()->getBillingIntervalAttribute(),$this->getBillingIntervalAttribute()) ?: 0,2); } /** * The base cost of this product at the appropriate billing interval including taxes * * @param Collection|NULL $taxes * @return float */ public function getBaseCostTaxableAttribute(Collection $taxes=NULL): float { return Tax::tax_calc($this->getBaseCostAttribute(),$taxes ?: config('site')->taxes);; } /** * Our default billing interval * Its the max of what we define, or what the supplier bills us at * * @return int */ public function getBillingIntervalAttribute(): int { return max($this->price_recur_default,$this->getSuppliedAttribute()->getBillingIntervalAttribute()); } /** * Return the type of service is provided. eg: Broadband, Phone. * * @return string|null */ public function getCategoryAttribute(): ?string { return $this->supplied ? $this->supplied->getCategoryAttribute() : NULL; } /** * 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 */ public function getCategoryNameAttribute(): string { return $this->supplied->getCategoryNameAttribute(); } /** * How long must this product be purchased for as a service. * * @return int */ public function getContractTermAttribute(): int { return $this->type->getContractTermAttribute(); } /** * This product full description * * @return string */ public function getDescriptionAttribute(): string { return (($x=$this->translate) && $x->description) ? $x->description : 'No Description'; } /** * Get the minimum cost of this product * * @param int|null $timeperiod * @param Group|null $go * @return float */ public function getMinChargeAttribute(int $timeperiod=NULL,Group $go=NULL): float { return $this->getSetupChargeAttribute($timeperiod,$go)+$this->getBaseChargeAttribute($timeperiod,$go)*Invoice::billing_term($this->getContractTermAttribute(),$this->getBillingIntervalAttribute()); } /** * Get the minimum cost of this product with taxes * * @param int|null $timeperiod * @param Group|null $go * @param Collection|NULL $taxes * @return float */ public function getMinChargeTaxableAttribute(int $timeperiod=NULL,Group $go=NULL,Collection $taxes=NULL): float { return Tax::tax_calc($this->getMinChargeAttribute($timeperiod,$go),$taxes ?: config('site')->taxes); } /** * Our products short descriptive name * * @return string */ public function getNameAttribute(): string { return $this->getNameShortAttribute().(($x=$this->getNameDetailAttribute()) ? ': '.$x : ''); } /** * Our products Long Name * * @return string */ public function getNameDetailAttribute(): string { return $this->translate ? $this->translate->name_detail : 'Unknown Name'; } /** * Our products PID * * @return string */ public function getNameShortAttribute(): string { return $this->translate ? $this->translate->name_short : 'Unknown PID'; } /** * Suppliers * * @return Model|null */ public function getSupplierAttribute(): ?Model { return $this->getSuppliedAttribute() ? $this->getSuppliedAttribute()->supplier_detail->supplier : NULL; } /** * Suppliers product * * @return Model * @todo make internal as other values off this attribute are used, not the model itself */ public function getSuppliedAttribute(): Model { return $this->type->supplied; } /** * The charge to setup this service * * @param int|null $timeperiod * @param Group|null $go * @return float */ public function getSetupChargeAttribute(int $timeperiod=NULL,Group $go=NULL): float { return $this->getCharge('setup',$timeperiod,$go); } /** * The charge to setup this service including taxes * * @param int|null $timeperiod * @param Group|null $go * @param Collection|null $taxes * @return float */ public function getSetupChargeTaxableAttribute(int $timeperiod=NULL,Group $go=NULL,Collection $taxes=NULL): float { return Tax::tax_calc($this->getSetupChargeAttribute($timeperiod,$go),$taxes ?: config('site')->taxes); } /** * The charge to setup this service * * @return float */ public function getSetupCostAttribute(): float { return $this->getSuppliedAttribute()->setup_cost ?: 0; } /** * The charge to setup this service * * @param Collection|null $taxes * @return float */ public function getSetupCostTaxableAttribute(Collection $taxes=NULL): float { return Tax::tax_calc($this->getSetupCostAttribute(),$taxes ?: config('site')->taxes);; } /* METHODS */ /** * Return the charge from the pricing table for the specific time period and group * * @param int $timeperiod * @param Group $go * @param string $type * @return float|null */ public function charge(int $timeperiod,Group $go,string $type): ?float { return Arr::get($this->pricing,sprintf('%d.%d.%s',$timeperiod,$go->id,$type)); } /** * Do we have a charge for specific group/period * * @param int $timeperiod * @return bool */ public function charge_available(int $timeperiod): bool { return Arr::get($this->pricing,sprintf('%d.show',$timeperiod),FALSE); } /** * Return a normalize price dependent on the product, ie: Broadband = Monthly, Domain = Yearly, etc * * @note: By definition products are normalised, as their cost price is based on the default billing interval * @return float */ public function cost_normalized(): float { return number_format(Tax::tax_calc($this->supplied->base_cost,config('site')->taxes),2); } /** * Get a charge value from the pricing array * * @param string $type * @param int|NULL $timeperiod * @param Group|NULL $go * @return float * @todo use self::charge() */ private function getCharge(string $type,int $timeperiod=NULL,Group $go=NULL): float { static $default = NULL; if (! $go) { if (is_null($default)) $default = Group::whereNull('parent_id')->firstOrFail(); // All public users $go = $default; } if (is_null($timeperiod)) $timeperiod = $this->getBillingIntervalAttribute(); // If the price doesnt exist for $go->id, use $go->id = 0 which is all users. if (! $price=Arr::get($this->pricing,sprintf('%d.%d.%s',$timeperiod,$go->id,$type))) { $alt_tp = $timeperiod; while (is_null($price=Arr::get($this->pricing,sprintf('%d.%d.%s',$alt_tp,0,$type))) && ($alt_tp >= 0)) { $alt_tp--; } // If we havent got a price, we'll extrapolate one, except for setup charges if (! is_null($price) && ($alt_tp !== $timeperiod) && ($type !== 'setup')) $price = $price*Invoice::billing_change($alt_tp,$timeperiod); } 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 * * @param Request $request * @return mixed */ public function orderValidation(Request $request): ?Model { return $this->type->orderValidation($request); } }