osb/app/Models/Product.php

487 lines
13 KiB
PHP
Raw Normal View History

2018-05-20 12:53:14 +00:00
<?php
namespace App\Models;
use Awobaz\Compoships\Compoships;
use Illuminate\Container\Container;
use Illuminate\Database\Eloquent\Factories\HasFactory;
2018-05-20 12:53:14 +00:00
use Illuminate\Database\Eloquent\Model;
2023-05-05 00:32:04 +00:00
use Illuminate\Http\Request;
2019-07-02 05:28:27 +00:00
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\File;
2023-05-05 00:32:04 +00:00
use Illuminate\Support\Str;
use Leenooks\Traits\ScopeActive;
2018-05-20 12:53:14 +00:00
use App\Interfaces\{IDs,ProductItem};
use App\Traits\{ProductDetails,SiteID};
2019-07-02 05:28:27 +00:00
2021-06-29 06:36:34 +00:00
/**
* Class Product
* Products that are available to sale, and appear on invoices.
*
* Products have one Type (Product/*), made of an Offering (Supplier/*) from a Supplier.
* Conversely, Suppliers provide Offerings (Supplier/*) which belong to a Type (Product/*) of a Product.
2021-06-29 06:36:34 +00:00
*
* So each product attribute has:
* + supplied : Supplier product provided for this offering (Supplier/*)
* + type : Returns the underlying product object, representing the type of product (Product/*)
*
2021-06-29 06:36:34 +00:00
* Attributes for products:
* + lid : Local ID for product (part number)
* + sid : System ID for product (part number)
* + category : Type of product supplied
* + category_name : Type of product supplied (Friendly Name for display, not for internal logic)
* + supplier : Supplier for this offering
* + name : Brief Name for our product with name_detail
* + name_short : Product ID for our Product (description.name => 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 ]
* ]
* ]
2021-06-29 06:36:34 +00:00
*
* @todo doesnt appear that price_type is used - but could be used to have different offering types billed differently
2021-06-29 06:36:34 +00:00
* @package App\Models
*/
class Product extends Model implements IDs
2018-05-20 12:53:14 +00:00
{
use Compoships,HasFactory,SiteID,ProductDetails,ScopeActive;
2020-02-12 10:32:57 +00:00
protected $casts = [
'pricing'=>'collection',
2020-02-12 10:32:57 +00:00
];
/* 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()
2018-05-20 12:53:14 +00:00
{
return $this->hasMany(Service::class);
2018-05-20 12:53:14 +00:00
}
/**
* 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()
2018-05-20 12:53:14 +00:00
{
return $this->hasOne(ProductTranslate::class)
2023-05-04 00:02:25 +00:00
->where('language_id',(Auth::user() && Auth::user()->language_id)
? Auth::user()->language_id
: config('site')->language_id);
2018-05-20 12:53:14 +00:00
}
2019-07-02 05:28:27 +00:00
/**
* Return a child model with details of the service
* This will return a product/* model.
2019-07-02 05:28:27 +00:00
*
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
public function type()
{
return $this->morphTo(null,'model','model_id');
2019-07-02 05:28:27 +00:00
}
/* INTERFACES */
2018-08-09 14:10:51 +00:00
public function getLIDAttribute(): string
2018-08-11 05:09:41 +00:00
{
return sprintf('%04s',$this->id);
2018-08-11 05:09:41 +00:00
}
public function getSIDAttribute(): string
2018-08-11 05:09:41 +00:00
{
return sprintf('%02s-%s',$this->site_id,$this->getLIDattribute());
2018-08-11 05:09:41 +00:00
}
/* ATTRIBUTES */
2018-08-11 05:09:41 +00:00
/**
* 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
2018-08-11 05:09:41 +00:00
{
2023-05-05 00:32:04 +00:00
return $this->_charge('base',$timeperiod,$go);
2018-08-11 05:09:41 +00:00
}
/**
* 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
2023-05-05 00:32:04 +00:00
* @deprecated move to account::tax_calc
*/
public function getBaseChargeTaxableAttribute(int $timeperiod=NULL,Group $go=NULL,Collection $taxes=NULL): float
2018-08-09 14:10:51 +00:00
{
return Tax::tax_calc($this->getBaseChargeAttribute($timeperiod,$go),$taxes ?: config('site')->taxes);
2018-08-09 14:10:51 +00:00
}
2021-06-29 06:36:34 +00:00
/**
* The base cost of this product at the appropriate billing interval
2021-06-29 06:36:34 +00:00
*
* @return float
2021-06-29 06:36:34 +00:00
*/
public function getBaseCostAttribute(): float
2021-06-29 06:36:34 +00:00
{
2023-05-05 00:32:04 +00:00
return $this->supplied->base_cost*Invoice::billing_change($this->supplied->billing_interval,$this->billing_interval) ?: 0;
2021-06-29 06:36:34 +00:00
}
/**
* The base cost of this product at the appropriate billing interval including taxes
*
* @param Collection|NULL $taxes
* @return float
2023-05-05 00:32:04 +00:00
* @deprecated move to account::tax_calc
*/
public function getBaseCostTaxableAttribute(Collection $taxes=NULL): float
2018-08-11 05:09:41 +00:00
{
return Tax::tax_calc($this->getBaseCostAttribute(),$taxes ?: config('site')->taxes);;
2018-08-11 05:09:41 +00:00
}
/**
* Our default billing interval
* Its the max of what we define, or what the supplier bills us at
*
* @return int
*/
public function getBillingIntervalAttribute(): int
2018-08-09 14:10:51 +00:00
{
2023-05-05 00:32:04 +00:00
return max($this->price_recur_default,$this->supplied->billing_interval);
2018-08-09 14:10:51 +00:00
}
/**
2023-05-05 00:32:04 +00:00
* This will return the category of the product (eg: domain, webhosting, etc) which is the basis for all
* other logic of these types.
*
2023-05-05 00:32:04 +00:00
* @return string
*/
2023-05-05 00:32:04 +00:00
public function getCategoryAttribute(): string
{
2023-05-05 00:32:04 +00:00
return strtolower(Str::studly($this->category_name));
}
/**
2023-05-05 00:32:04 +00:00
* Return the type of service is provided. eg: Broadband, Phone.
* 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
{
2023-05-05 00:32:04 +00:00
return $this->supplied->category_name;
}
/**
* How long must this product be purchased for as a service.
*
* @return int
*/
public function getContractTermAttribute(): int
2019-07-02 05:28:27 +00:00
{
2023-05-05 00:32:04 +00:00
return $this->type->contract_term;
2019-07-02 05:28:27 +00:00
}
/**
* This product full description
*
* @return string
*/
public function getDescriptionAttribute(): string
{
2023-05-05 00:32:04 +00:00
return $this->translate->description;
}
/**
2023-05-05 00:32:04 +00:00
* Get the minimum charge for this product
*
* @param int|null $timeperiod
* @param Group|null $go
* @return float
*/
public function getMinChargeAttribute(int $timeperiod=NULL,Group $go=NULL): float
2018-08-11 05:09:41 +00:00
{
2023-05-05 00:32:04 +00:00
return $this->getSetupChargeAttribute($timeperiod,$go)+$this->getBaseChargeAttribute($timeperiod,$go)*Invoice::billing_term($this->type->contract_term,$this->getBillingIntervalAttribute());
2018-08-11 05:09:41 +00:00
}
/**
* Get the minimum cost of this product with taxes
*
* @param int|null $timeperiod
* @param Group|null $go
* @param Collection|NULL $taxes
* @return float
2023-05-05 00:32:04 +00:00
* @deprecated move to account::tax_calc
*/
public function getMinChargeTaxableAttribute(int $timeperiod=NULL,Group $go=NULL,Collection $taxes=NULL): float
2018-08-11 05:09:41 +00:00
{
return Tax::tax_calc($this->getMinChargeAttribute($timeperiod,$go),$taxes ?: config('site')->taxes);
2018-08-11 05:09:41 +00:00
}
/**
* Our products short descriptive name
*
* @return string
*/
public function getNameAttribute(): string
2018-08-11 05:09:41 +00:00
{
return $this->getNameShortAttribute().(($x=$this->getNameDetailAttribute()) ? ': '.$x : '');
2018-08-11 05:09:41 +00:00
}
/**
* Our products Long Name
*
* @return string
*/
public function getNameDetailAttribute(): string
2018-08-01 07:09:38 +00:00
{
2023-05-05 00:32:04 +00:00
return $this->translate->name_detail;
2018-08-01 07:09:38 +00:00
}
/**
* Our products PID
*
* @return string
*/
public function getNameShortAttribute(): string
2018-08-11 05:09:41 +00:00
{
2023-05-05 00:32:04 +00:00
return $this->translate->name_short;
2018-08-11 05:09:41 +00:00
}
/**
2023-05-05 00:32:04 +00:00
* Suppliers product
*
2023-05-05 00:32:04 +00:00
* @return Model
*/
2023-05-05 00:32:04 +00:00
public function getSuppliedAttribute(): Model
{
2023-05-05 00:32:04 +00:00
return $this->type->supplied;
}
/**
2023-05-05 00:32:04 +00:00
* Suppliers of this product
*
2023-05-05 00:32:04 +00:00
* @return Model|null
*/
2023-05-05 00:32:04 +00:00
public function getSupplierAttribute(): ?Model
{
2023-05-05 00:32:04 +00:00
return $this->supplied->supplier;
2021-06-29 06:36:34 +00:00
}
/**
* 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
{
2023-05-05 00:32:04 +00:00
return $this->_charge('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
2023-05-05 00:32:04 +00:00
* @deprecated move to account::tax_calc
*/
public function getSetupChargeTaxableAttribute(int $timeperiod=NULL,Group $go=NULL,Collection $taxes=NULL): float
2018-08-09 14:10:51 +00:00
{
return Tax::tax_calc($this->getSetupChargeAttribute($timeperiod,$go),$taxes ?: config('site')->taxes);
2018-08-09 14:10:51 +00:00
}
/**
2023-05-05 00:32:04 +00:00
* The cost to setup this service
*
* @return float
*/
public function getSetupCostAttribute(): float
2018-08-09 14:10:51 +00:00
{
2023-05-05 00:32:04 +00:00
return $this->supplied->setup_cost ?: 0;
2018-08-09 14:10:51 +00:00
}
/**
* The charge to setup this service
*
* @param Collection|null $taxes
* @return float
2023-05-05 00:32:04 +00:00
* @deprecated move to account::tax_calc
*/
public function getSetupCostTaxableAttribute(Collection $taxes=NULL): float
2018-08-09 14:10:51 +00:00
{
return Tax::tax_calc($this->getSetupCostAttribute(),$taxes ?: config('site')->taxes);;
2018-08-11 05:09:41 +00:00
}
/* METHODS */
2018-08-11 05:09:41 +00:00
2023-05-05 00:32:04 +00:00
/**
* 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->getBillingIntervalAttribute();
// 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);
}
/**
* 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
2019-07-02 05:28:27 +00:00
{
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
2023-05-05 00:32:04 +00:00
* @todo Move tax calculations out
*/
public function cost_normalized(): float
{
return number_format(Tax::tax_calc($this->supplied->base_cost,config('site')->taxes),2);
2019-07-02 05:28:27 +00:00
}
/**
* Return if this product captures usage data
*
* @return bool
*/
public function hasUsage(): bool
{
return $this->type->hasUsage();
}
2018-05-20 12:53:14 +00:00
/**
* When receiving an order, validate that we have all the required information for the product type
2018-05-20 12:53:14 +00:00
*
* @param Request $request
2023-05-05 00:32:04 +00:00
* @return Model|null
2018-05-20 12:53:14 +00:00
*/
public function orderValidation(Request $request): ?Model
2018-05-20 12:53:14 +00:00
{
return $this->type->orderValidation($request);
2018-05-20 12:53:14 +00:00
}
}