description_full) * + has_usage : Does this product instrument usage * * + min_cost : Minimum cost for this product * * + name : Details of our product (description.description_short => name_detail) * + pid : Product ID for our Product (description.name => name_short) * * + setup_cost : Charge by supplier to setup this product * * + supplied : Suppliers product supplied * + supplier : Supplier for this offering * * 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 ] * ] * ] */ class Product extends Model { use HasFactory,ScopeActive,ProviderRef,ProviderTokenTrait; protected $casts = [ 'pricing' => CollectionOrNull::class, ]; public function __get($key): mixed { return match ($key) { 'base_cost' => round($this->supplied->base_cost,2)*Invoice::billing_change($this->type->billing_interval,$this->billing_interval) ?: 0, 'billing_interval' => max($this->price_recur_default,$this->type->billing_interval), 'billing_interval_name' => Invoice::billing_name($this->billing_interval), 'category' => $this->supplied->category, 'category_lc' => strtolower($this->category), 'category_name' => $this->supplied->category_name, 'contract_term' => $this->type->contract_term, 'description' => $this->translate->description, 'has_usage' => $this->type->hasUsage(), 'min_cost' => $this->supplied->min_cost, 'name' => $this->translate->name_detail, 'lid' => sprintf('%04s',$this->id), 'pid' => $this->translate->name_short, 'sid' => sprintf('%02s-%s',$this->site_id,$this->lid), 'setup_cost' => $this->supplied->setup_cost ?: 0, 'supplied' => $this->type->supplied, 'supplier' => $this->supplied->supplier, default => parent::__get($key), }; } /* 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 */ public function providers() { return $this->belongsToMany(ProviderOauth::class,'product__provider') ->where('product__provider.site_id',$this->site_id) ->withPivot('ref'); } /** * 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('osb.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'); } /* METHODS */ /** * Get a charge value from the pricing array * * This will traverse the groups to the parent group to find an appropriate charge, if one is not found, * it will extrapolate a charge from the parent group, from another time period. * * @param string $type * @param int|NULL $timeperiod * @param Group|NULL $go * @return float */ private function _charge(string $type,?int $timeperiod=NULL,?Group $go=NULL): float { // We'll cache this for performance static $default = NULL; // All public users if (is_null($default)) $default = Group::whereNull('parent_id')->firstOrFail(); if (! $go) $go = $default; if (is_null($timeperiod)) $timeperiod = $this->billing_interval; // If the price doesnt exist for $go->id, use $go->id = 0 which is all users. if (! $price=$this->charge($timeperiod,$go,$type)) { $alt_tp = $timeperiod; // Traverse up our timeperiods if the default group to see if there is another time period that has the value while (is_null($price=$this->charge($alt_tp,$default,$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); } public function accounting(): Collection { $so = ProviderOauth::where('name',self::provider) ->sole(); if (! ($to=$so->token(Auth::user()))) throw new NotTokenException(sprintf('Unknown Tokens for [%s]',Auth::user()->email)); return $to ->API() ->getItems() ->pluck('pid','Id') ->map(fn($item,$value)=>['id'=>$value,'value'=>$item]) ->values(); } /** * The amount we invoice each time period for this service * * @param int|NULL $timeperiod * @param Group|NULL $go * @return float */ public function base_charge(?int $timeperiod=NULL,?Group $go=NULL): float { return $this->_charge('base',$timeperiod,$go); } /** * 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); } /** * Get the minimum charge for this product * * @param int|null $timeperiod * @param Group|null $go * @return float */ public function min_charge(?int $timeperiod=NULL,?Group $go=NULL): float { return $this->setup_charge($timeperiod,$go) + $this->base_charge($timeperiod,$go)*Invoice::billing_change($this->billing_interval,$this->type->billing_interval)*$this->contract_term; } /** * When receiving an order, validate that we have all the required information for the product type * * @param Request $request * @return Model|null */ public function orderValidation(Request $request): ?Model { return $this->type->orderValidation($request); } /** * The charge to setup this service * * @param int|null $timeperiod * @param Group|null $go * @return float */ public function setup_charge(?int $timeperiod=NULL,?Group $go=NULL): float { return $this->_charge('setup',$timeperiod,$go); } }