Rework service, removed redundant code, service invoicing improvements

This commit is contained in:
2024-07-29 23:12:53 +10:00
parent 5f10175b35
commit 0b5bc9e012
29 changed files with 474 additions and 523 deletions

View File

@@ -16,7 +16,7 @@ use Leenooks\Casts\LeenooksCarbon;
use App\Models\Product\Type;
use App\Interfaces\IDs;
use App\Traits\ScopeServiceUserAuthorised;
use App\Traits\{ScopeServiceActive,ScopeServiceUserAuthorised};
/**
* Class Service
@@ -29,10 +29,10 @@ use App\Traits\ScopeServiceUserAuthorised;
*
* Attributes for services:
* + additional_cost : Pending additional charges for this service (excluding setup) //@todo check all these are still valid
* + billing_charge : Charge for this service each invoice period // @todo change to "charge"
* + billing_charge : 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
* + billed_to : When this service has been billed to // @todo rename all references to invoice_to
* + invoiced_to : When this service has been billed to
* + category : The type of service this is, eg: broadband, phone
* + category_name : The type of service this is, eg: Broadband, Telephone (in human friendly)
* + contract_term : The term that this service must be active
@@ -54,7 +54,7 @@ use App\Traits\ScopeServiceUserAuthorised;
*/
class Service extends Model implements IDs
{
use HasFactory,ScopeServiceUserAuthorised;
use HasFactory,ScopeServiceActive,ScopeServiceUserAuthorised;
protected $casts = [
'order_info' => AsCollection::class,
@@ -281,8 +281,8 @@ class Service extends Model implements IDs
public static function movements(User $uo): Collection
{
return (new self)
->active()
->serviceUserAuthorised($uo)
->ServiceActive()
->ServiceUserAuthorised($uo)
->where('order_status','!=','ACTIVE')
->with(['account','product'])
->get();
@@ -343,7 +343,7 @@ class Service extends Model implements IDs
public function charges_active()
{
return $this->charges()
->active();
->ServiceActive();
}
/**
@@ -371,6 +371,7 @@ class Service extends Model implements IDs
*/
public function invoice_items($active=TRUE)
{
Log::alert('Call to deprecated functon '.__METHOD__);
return $this->invoiced_items_active();
}
@@ -418,6 +419,7 @@ class Service extends Model implements IDs
{
return $this->hasMany(InvoiceItem::class)
->where('item_type','=',0)
->whereNotNull('start_at')
->orderBy('start_at','desc');
}
@@ -464,9 +466,11 @@ class Service extends Model implements IDs
/**
* Only query active categories
* @deprecated use ScopeServiceActive
*/
public function scopeActive($query)
{
throw new \Exception('deprecated');
return $query->where(
fn($query)=>
$query->where($this->getTable().'.active',TRUE)
@@ -479,9 +483,11 @@ class Service extends Model implements IDs
*
* @param $query
* @return mixed
* @deprecated use ScopeServiceInactive
*/
public function scopeInActive($query)
public function scopeInactive($query)
{
dd('deprecated');
return $query->where(
fn($query)=>
$query->where($this->getTable().'.active',FALSE)
@@ -527,15 +533,7 @@ class Service extends Model implements IDs
*/
public function getBillingChargeAttribute(): float
{
// If recur_schedule is null, then we only bill this item once
if (is_null($this->recur_schedule) && $this->getInvoiceToAttribute())
$this->price = 0;
return $this->account->taxed(
is_null($this->price)
? $this->product->getBaseChargeAttribute($this->recur_schedule,$this->account->group)
: $this->price
);
return $this->account->taxed($this->billing_charge());
}
/**
@@ -546,7 +544,7 @@ class Service extends Model implements IDs
*/
public function getBillingChargeNormalisedAttribute(): float
{
return number_format($this->getBillingChargeAttribute()*Invoice::billing_change($this->recur_schedule,$this->offering->billing_interval),2);
return number_format($this->getBillingChargeAttribute()*Invoice::billing_change($this->getBillingIntervalAttribute(),$this->offering->billing_interval),2);
}
/**
@@ -569,39 +567,6 @@ class Service extends Model implements IDs
return Invoice::billing_name($this->getBillingIntervalAttribute());
}
/**
* Determine a monthly price for a service, even if it is billed at a different frequency
*
* @return float
* @throws Exception
* @deprecated use class::billing_charge_normalised()
*/
public function getBillingMonthlyPriceAttribute(): float
{
Log::alert('SMO:! Deprecated function getBillingMonthlyPriceAttribute()');
return $this->getBillingChargeNormalisedAttribute();
}
/**
* Service Category ID
*
* @return string
*/
public function getCategoryAttribute(): string
{
return $this->product->category;
}
/**
* Service Category Name
*
* @return string
*/
public function getCategoryNameAttribute(): string
{
return $this->product->category_name;
}
/**
* The date the contract ends
*
@@ -652,155 +617,19 @@ class Service extends Model implements IDs
*/
public function getInvoiceNextAttribute(): Carbon
{
$last = $this->getInvoiceToAttribute();
$last = $this->getInvoicedToAttribute();
return $last
? $last->addDay()
: (min($this->start_at,$this->invoice_next_at) ?: Carbon::now());
}
/**
* Return the end date for the next invoice
*
* @return Carbon
* @throws Exception
*/
public function getInvoiceNextEndAttribute(): Carbon
{
switch ($this->recur_schedule) {
case Invoice::BILL_WEEKLY:
$date = $this->product->price_recur_strict
? $this->getInvoiceNextAttribute()->endOfWeek()
: $this->getInvoiceNextAttribute()->addWeek()->subDay();
break;
case Invoice::BILL_MONTHLY:
$date = $this->product->price_recur_strict
? $this->getInvoiceNextAttribute()->endOfMonth()
: $this->getInvoiceNextAttribute()->addMonth()->subDay();
break;
case Invoice::BILL_QUARTERLY:
$date = $this->product->price_recur_strict
? $this->getInvoiceNextAttribute()->endOfQuarter()
: $this->getInvoiceNextAttribute()->addQuarter()->subDay();
break;
case Invoice::BILL_SEMI_YEARLY:
$date = $this->product->price_recur_strict
? $this->getInvoiceNextAttribute()->endOfHalf()
: $this->getInvoiceNextAttribute()->addQuarters(2)->subDay();
break;
case Invoice::BILL_YEARLY:
$date = $this->product->price_recur_strict
? $this->getInvoiceNextAttribute()->endOfYear()
: $this->getInvoiceNextAttribute()->addYear()->subDay();
break;
case Invoice::BILL_TWOYEARS:
if (! $this->product->price_recur_strict) {
$date = $this->getInvoiceNextAttribute()->addYears(2)->subDay();
} else {
$date = $this->getInvoiceNextAttribute()->addYears(2)->subDay()->endOfYear();
// Make sure we end on an even year
if ($date->clone()->addDay()->year%2)
$date = $date->subYear();
}
break;
// NOTE: price_recur_strict ignored
case Invoice::BILL_THREEYEARS:
$date = $this->getInvoiceNextAttribute()->addYears(3)->subDay();
break;
// NOTE: price_recur_strict ignored
case Invoice::BILL_FOURYEARS:
$date = $this->getInvoiceNextAttribute()->addYears(4)->subDay();
break;
// NOTE: price_recur_strict ignored
case Invoice::BILL_FIVEYEARS:
$date = $this->getInvoiceNextAttribute()->addYears(5)->subDay();
break;
default:
throw new Exception('Unknown recur_schedule');
}
// If the invoice has an end date, our invoice period shouldnt be greater than that (might be terminating).
if ($this->stop_at && ($this->stop_at < $date))
$date = $this->stop_at;
return $date;
}
/**
* Determine how much quantity (at the charge rate) is required for the next invoice
*
* @return float
* @throws Exception
*/
public function getInvoiceNextQuantityAttribute(): float
{
// If we are not rounding to the first day of the cycle, then it is always a full cycle
if (! $this->product->price_recur_strict)
return 1;
$n = $this->invoice_next->diffInDays($this->invoice_next_end);
switch ($this->recur_schedule) {
case Invoice::BILL_WEEKLY:
$d = $this->invoice_next_end->addWeek()->startOfWeek()->diffInDays($this->invoice_next->startOfWeek());
break;
case Invoice::BILL_MONTHLY:
$d = $this->invoice_next_end->addMonth()->startOfMonth()->diffInDays($this->invoice_next->startOfMonth());
break;
case Invoice::BILL_QUARTERLY:
$d = $this->invoice_next_end->startOfQuarter()->diffInDays($this->invoice_next->addQuarter()->startOfQuarter());
break;
case Invoice::BILL_SEMI_YEARLY:
$d = $this->invoice_next_end->addQuarter(2)->startOfHalf()->diffInDays($this->invoice_next->startOfHalf());
break;
case Invoice::BILL_YEARLY:
$d = $this->invoice_next_end->addYear()->startOfYear()->diffInDays($this->invoice_next->startOfYear());
break;
case Invoice::BILL_TWOYEARS:
$d = $this->invoice_next_end->addYear(2)->startOfYear()->diffInDays($this->invoice_next->subyear(2))-1;
break;
case Invoice::BILL_THREEYEARS:
$d = $this->invoice_next_end->addYear(3)->startOfYear()->diffInDays($this->invoice_next->subyear(3))-1;
break;
case Invoice::BILL_FOURYEARS:
$d = $this->invoice_next_end->addYear(3)->startOfYear()->diffInDays($this->invoice_next->subyear(4))-1;
break;
case Invoice::BILL_FIVEYEARS:
$d = $this->invoice_next_end->addYear(3)->startOfYear()->diffInDays($this->invoice_next->subyear(5))-1;
break;
default:
throw new Exception('Unknown recur_schedule');
}
return round($n/$d,2);
}
/**
* Get the date that the service has been invoiced to
*
* @return Carbon|null
*/
public function getInvoiceToAttribute(): ?Carbon
public function getInvoicedToAttribute(): ?Carbon
{
return ($x=$this->invoiced_service_items_active_recent)->count()
? $x->first()->stop_at
@@ -826,7 +655,9 @@ class Service extends Model implements IDs
*/
public function getNameShortAttribute()
{
return $this->type->getServiceNameAttribute() ? $this->type->getServiceNameAttribute() : 'SID:'.$this->sid;
return $this->type->getServiceNameAttribute()
? $this->type->getServiceNameAttribute()
: 'SID:'.$this->sid;
}
/**
@@ -841,7 +672,9 @@ class Service extends Model implements IDs
*/
public function getNameDetailAttribute()
{
return ($this->type->getServiceDescriptionAttribute() !== NULL) ? $this->type->getServiceDescriptionAttribute() : 'No Description';
return ($this->type->getServiceDescriptionAttribute() !== NULL)
? $this->type->getServiceDescriptionAttribute()
: 'No Description';
}
/**
@@ -878,22 +711,13 @@ class Service extends Model implements IDs
->last();
return $lastpaid
? $this->invoiced_service_items_active->where('invoice_id',$lastpaid->id)->where('type',0)->max('stop_at')
? $this->invoiced_service_items_active
->where('invoice_id',$lastpaid->id)
->where('type',0)
->max('stop_at')
: NULL;
}
/**
* Return the billing recurring configuration for this service
*
* @param $value
* @return int
*/
public function xgetRecurScheduleAttribute($value): int
{
// If recur_schedule not set, default to quarterly
return $value ?? Invoice::BILL_QUARTERLY;
}
/**
* Return the Service Status
*
@@ -1033,6 +857,7 @@ class Service extends Model implements IDs
public function actions(): Collection
{
$next = $this->getStageParameters($this->order_status)->get('next');
return $next
? $next->map(function($item,$key) {
$authorized = FALSE;
@@ -1050,6 +875,22 @@ class Service extends Model implements IDs
: collect();
}
/**
* This service billing charge, pre-taxes
*
* @return float
*/
public function billing_charge(): float
{
// If recur_schedule is null, then we only bill this item once
if (is_null($this->getBillingIntervalAttribute()) && $this->getInvoicedToAttribute())
$this->price = 0;
return is_null($this->price)
? $this->product->getBaseChargeAttribute($this->getBillingIntervalAttribute(),$this->account->group)
: $this->price;
}
/**
* Get the stage parameters
*
@@ -1108,7 +949,8 @@ class Service extends Model implements IDs
*/
public function isActive(): bool
{
return $this->attributes['active'] || ($this->order_status && (! in_array($this->order_status,self::INACTIVE_STATUS)));
return $this->attributes['active']
|| ($this->order_status && (! in_array($this->order_status,self::INACTIVE_STATUS)));
}
/**
@@ -1180,17 +1022,6 @@ class Service extends Model implements IDs
return ! is_null($this->price);
}
/**
* Should this service be invoiced soon
*
* @param int $days
* @return bool
*/
public function isInvoiceDueSoon($days=30): bool
{
return $this->isBilled() AND $this->getInvoiceNextAttribute()->lessThan(now()->addDays($days));
}
/**
* Identify if a service is being ordered, ie: not active yet nor cancelled
*
@@ -1206,95 +1037,88 @@ class Service extends Model implements IDs
/**
* Generate a collection of invoice_item objects that will be billed for the next invoice
*
* @param bool $future Next item to be billed (not in the next x days)
* @param Carbon|null $billdate
* @return Collection
* @throws Exception
* @todo This query is expensive.
*/
public function next_invoice_items(bool $future,Carbon $billdate=NULL): Collection
public function next_invoice_items(Carbon $billdate=NULL): Collection
{
if ($this->wasCancelled() OR (! $this->isBilled()) OR (! $future AND ! $this->active))
if ($this->wasCancelled() || (! $this->isBilled()))
return collect();
if (is_null($billdate))
$billdate = Carbon::now()->addDays(30);
$o = collect();
$invoiced_to = $this->getInvoiceNextAttribute();
// If pending, add any connection charges
// Connection charges are only charged once
if ((! $this->invoice_items->filter(function($item) { return $item->item_type==4; })->sum('total'))
AND ($this->isPending() OR is_null($this->invoice_to))
AND $this->product->getSetupChargeAttribute($this->recur_schedule,$this->account->group))
// Connection charges are only charged once, so ignore if if we have already billed them
if ((! $this->invoiced_items()->where('item_type',InvoiceItem::INVOICEITEM_SETUP)->count())
&& (InvoiceItem::distinct('invoice_id')->where('service_id',$this->id)->count() < 2)
&& $this->product->getSetupChargeAttribute($this->getBillingIntervalAttribute(),$this->account->group))
{
$o = new InvoiceItem;
$ii = new InvoiceItem;
$o->active = TRUE;
$o->service_id = $this->id;
$o->product_id = $this->product_id;
$o->item_type = 4; // @todo change to const or something
$o->price_base = $this->product->getSetupChargeAttribute($this->recur_schedule,$this->account->group);
//$o->recurring_schedule = $this->recur_schedule;
$o->start_at = $this->invoice_next;
$o->stop_at = $this->invoice_next;
$o->quantity = 1;
$o->site_id = 1; // @todo
$ii->active = TRUE;
$ii->service_id = $this->id;
$ii->product_id = $this->product_id;
$ii->item_type = InvoiceItem::INVOICEITEM_SETUP;
$ii->price_base = $this->product->getSetupChargeAttribute($this->getBillingIntervalAttribute(),$this->account->group);
$ii->start_at = $this->invoice_next;
$ii->stop_at = $this->invoice_next;
$ii->quantity = 1;
$ii->site_id = 1; // @todo
$o->addTaxes($this->account->country->taxes);
$this->invoice_items->push($o);
$ii->addTaxes($this->account->country->taxes);
$o->push($ii);
}
// If the service is active, there will be service charges
if ((! $this->invoice_items->filter(function($item) { return $item->item_type==0 AND ! $item->exists; })->count())
AND ($this->active OR $this->isPending())
AND (
(($future == TRUE) AND $this->invoice_next < $this->invoice_next_end) OR
(($future == FALSE) AND ($this->invoice_to < ($this->stop_at ?: $billdate)))
))
{
do {
$o = new InvoiceItem;
$o->active = TRUE;
$o->service_id = $this->id;
$o->product_id = $this->product_id;
$o->item_type = 0;
$o->price_base = is_null($this->price)
? (is_null($this->price) ? $this->product->getBaseChargeAttribute($this->recur_schedule,$this->account->group) : $this->price)
: $this->price; // @todo change to a method in this class
$o->recur_schedule = $this->recur_schedule;
$o->start_at = $this->invoice_next;
$o->stop_at = $this->invoice_next_end;
$o->quantity = $this->invoice_next_quantity;
$o->site_id = 1; // @todo
// The service charges
if (is_null($billdate))
$billdate = $invoiced_to->clone()->addDays(config('osb.invoice_days'));
$o->addTaxes($this->account->country->taxes);
$this->invoice_items->push($o);
} while ($future == FALSE AND ($this->invoice_to < ($this->stop_at ?: $billdate)));
while ($invoiced_to < ($this->stop_at ?: $billdate)) {
$ii = new InvoiceItem;
$period = Invoice::invoice_period($invoiced_to,$this->getBillingIntervalAttribute(),$this->product->price_recur_strict);
$ii->active = TRUE;
$ii->service_id = $this->id;
$ii->product_id = $this->product_id;
$ii->item_type = InvoiceItem::INVOICEITEM_SERVICE;
$ii->price_base = $this->billing_charge();
$ii->recur_schedule = $this->getBillingIntervalAttribute();
$ii->start_at = $invoiced_to;
$ii->stop_at = ($this->stop_at && ($this->stop_at < Arr::get($period,'end'))) ? $this->stop_at : Arr::get($period,'end');
$ii->quantity = Invoice::invoice_quantity($ii->start_at,$ii->stop_at,$period);
$ii->site_id = 1; // @todo
$ii->addTaxes($this->account->country->taxes);
$o->push($ii);
$invoiced_to = $ii->stop_at
->clone()
->addDay()
->startOfDay();
}
// Add additional charges
if ((($future == TRUE) OR (($future == FALSE) AND ($this->invoice_to >= $billdate)))
AND ! $this->invoice_items->filter(function($item) { return $item->module_id == 30 AND ! $item->exists; })->count())
{
foreach ($this->charges->filter(function($item) { return $item->unprocessed; }) as $oo) {
$o = new InvoiceItem;
$o->active = TRUE;
$o->service_id = $oo->service_id;
$o->product_id = $this->product_id;
$o->quantity = $oo->quantity;
$o->item_type = $oo->type;
$o->price_base = $oo->amount;
$o->start_at = $oo->start_at;
$o->stop_at = $oo->stop_at;
$o->module_id = 30; // @todo This shouldnt be hard coded
$o->module_ref = $oo->id;
$o->site_id = 1; // @todo
foreach ($this->charges->filter(function($item) { return $item->unprocessed; }) as $oo) {
$ii = new InvoiceItem;
$o->addTaxes($this->account->country->taxes);
$this->invoice_items->push($o);
}
$ii->active = TRUE;
$ii->service_id = $oo->service_id;
$ii->product_id = $this->product_id;
$ii->quantity = $oo->quantity;
$ii->item_type = $oo->type;
$ii->price_base = $oo->amount;
$ii->start_at = $oo->start_at;
$ii->stop_at = $oo->stop_at;
$ii->module_id = 30; // @todo This shouldnt be hard coded
$ii->module_ref = $oo->id;
$ii->site_id = 1; // @todo
$ii->addTaxes($this->account->country->taxes);
$o->push($ii);
}
return $this->invoice_items->filter(function($item) { return ! $item->exists; });
return $o;
}
/**