Move billing more service::class methods into __get(), use of component ribbons for service status

This commit is contained in:
Deon George 2025-05-21 18:05:27 +10:00
parent 14c0109efa
commit 15a71c9f5b
14 changed files with 63 additions and 134 deletions

View File

@ -49,7 +49,7 @@ class ServiceList extends Command
)); ));
foreach (Service::cursor() as $o) { foreach (Service::cursor() as $o) {
if ((! $this->option('inactive')) && (! $o->isActive())) if ((! $this->option('inactive')) && (! $o->is_active))
continue; continue;
if ($this->option('type') && ($o->product->getCategoryAttribute() !== $this->option('type'))) if ($this->option('type') && ($o->product->getCategoryAttribute() !== $this->option('type')))

View File

@ -43,14 +43,20 @@ use App\Traits\{ScopeAccountUserAuthorised,ScopeServiceActive,SiteID};
* + billing_cost_orig_taxed : Cost for this service before being overridden by $this->cost (with Account TAX) * + billing_cost_orig_taxed : Cost for this service before being overridden by $this->cost (with Account TAX)
* + billing_interval : The period that this service is billed * + billing_interval : The period that this service is billed
* + billing_interval_name : The period that this service is billed as a name * + billing_interval_name : The period that this service is billed as a name
* + contract_term : The term that this service must be active
* + is_active : Is this service active. It is active, if active=true, or the order_status is not in self::INACTIVE_STATUS[]
* + is_billed : Does this service generate an invoice * + is_billed : Does this service generate an invoice
* + is_cancelled : Service that has been cancelled or never provisioned
* + is_charge_overridden : Has the price been overridden * + is_charge_overridden : Has the price been overridden
* + is_contracted : Is this service on a contract
* + is_cost_overridden : Has the cost been overridden * + is_cost_overridden : Has the cost been overridden
* + is_pending_active : Has this service been ordered, waiting to be active
* + is_pending_change : Is this service changing
* + is_pending_cancel : Is this active service being cancelled
* + status : Service status (inactive/active)
* *
* Attributes for services (OLD): * Attributes for services (OLD):
* + additional_cost : Pending additional charges for this service (excluding setup) //@todo check all these are still valid
* + invoiced_to : When this service has been billed to * + invoiced_to : When this service has been billed to
* + contract_term : The term that this service must be active
* + contract_end : The date that the contract ends for this service * + contract_end : The date that the contract ends for this service
* + name : Service short name with service address * + name : Service short name with service address
* + name_short : Service Product short name, eg: phone number, domain name, certificate CN * + name_short : Service Product short name, eg: phone number, domain name, certificate CN
@ -58,12 +64,6 @@ use App\Traits\{ScopeAccountUserAuthorised,ScopeServiceActive,SiteID};
* + product : Our product that is providing this service * + product : Our product that is providing this service
* + sid : System ID for service * + sid : System ID for service
* + supplied : The model of the supplier's product used for this service. * + supplied : The model of the supplier's product used for this service.
*
* Methods:
* + isPending : Is this a pending active service
*
* @package App\Models
* @todo Add min_charge
*/ */
class Service extends Model implements IDs class Service extends Model implements IDs
{ {
@ -301,9 +301,22 @@ class Service extends Model implements IDs
'billing_interval' => $this->recur_schedule ?: $this->product->billing_interval, 'billing_interval' => $this->recur_schedule ?: $this->product->billing_interval,
'billing_interval_name' => Invoice::billing_name($this->billing_interval), 'billing_interval_name' => Invoice::billing_name($this->billing_interval),
'contract_term' => max($this->supplied->contract_term,$this->product->type->contract_term),
'is_active' => $this->active || ($this->order_status && (! in_array($this->order_status,self::INACTIVE_STATUS))),
'is_billed' => (! ($this->external_billing || $this->suspend_billing || ($this->price === 0))), 'is_billed' => (! ($this->external_billing || $this->suspend_billing || ($this->price === 0))),
'is_cancelled' => in_array($this->order_status,self::INACTIVE_STATUS),
'is_charge_overridden' => (! is_null($this->price)), 'is_charge_overridden' => (! is_null($this->price)),
'is_contracted' => $this->getContractEndAttribute() && $this->getContractEndAttribute()->greaterThan(Carbon::now()),
'is_cost_overridden' => (! is_null($this->cost)), 'is_cost_overridden' => (! is_null($this->cost)),
'is_pending_active' => (! $this->active) && (! is_null($this->order_status)) && (! in_array($this->order_status,array_merge(self::INACTIVE_STATUS,['INACTIVE']))),
'is_pending_change' => $this->active && $this->changes()->where('service__change.active',TRUE)->where('complete',FALSE)->count(),
'is_pending_cancel' => $this->active && in_array(strtolower($this->order_status),['cancel-request','cancel-pending']),
'status' => $this->active
? strtolower($this->order_status)
: ((strtolower($this->order_status) === 'cancelled') ? 'cancelled' : 'inactive'),
default => parent::__get($key), default => parent::__get($key),
}; };
} }
@ -571,7 +584,7 @@ class Service extends Model implements IDs
if (! $this->start_at) if (! $this->start_at)
return $this->type->expire_at; return $this->type->expire_at;
$end = $this->start_at->clone()->addMonths($this->getContractTermAttribute()); $end = $this->start_at->clone()->addMonths($this->contract_term);
// If we dont have an expire date, use the start date + contract_term // If we dont have an expire date, use the start date + contract_term
if (! $this->type->expire_at) if (! $this->type->expire_at)
@ -581,17 +594,6 @@ class Service extends Model implements IDs
return ($end < $this->type->expire_at) ? $this->type->expire_at : $end; return ($end < $this->type->expire_at) ? $this->type->expire_at : $end;
} }
/**
* This function will determine the minimum contract term for a service, which is the maximum of
* supplied->contract_term, or the product->type->contract_term;
*
* @return int
*/
public function getContractTermAttribute(): int
{
return max($this->supplied->contract_term,$this->product->type->contract_term);
}
/** /**
* Return the date for the next invoice * Return the date for the next invoice
* *
@ -645,9 +647,7 @@ class Service extends Model implements IDs
*/ */
public function getNameShortAttribute() public function getNameShortAttribute()
{ {
return $this->type->getServiceNameAttribute() return $this->type->getServiceNameAttribute() ?: 'SID:'.$this->sid;
? $this->type->getServiceNameAttribute()
: 'SID:'.$this->sid;
} }
/** /**
@ -708,18 +708,6 @@ class Service extends Model implements IDs
: NULL; : NULL;
} }
/**
* Return the Service Status
*
* @return string
*/
public function getStatusAttribute(): string
{
return $this->active
? strtolower($this->order_status)
: ((strtolower($this->order_status) === 'cancelled') ? 'cancelled' : 'inactive');
}
/** /**
* Return the product that supplies this service * Return the product that supplies this service
* ie: product/* * ie: product/*
@ -898,7 +886,7 @@ class Service extends Model implements IDs
$max = max($date,$this->getCancelDateAttribute())->clone(); $max = max($date,$this->getCancelDateAttribute())->clone();
if (! $this->getPaidToAttribute()) if (! $this->getPaidToAttribute())
return $this->getContractTermAttribute()*$this->billing_charge_normalised_taxed; return $this->contract_term*$this->billing_charge_normalised_taxed;
if ($this->getPaidToAttribute()->lessThan($max)) { if ($this->getPaidToAttribute()->lessThan($max)) {
$d = $this->getPaidToAttribute()->diffInDays($max); $d = $this->getPaidToAttribute()->diffInDays($max);
@ -938,7 +926,7 @@ class Service extends Model implements IDs
$max = max($date,$this->getCancelDateAttribute())->clone(); $max = max($date,$this->getCancelDateAttribute())->clone();
if (! $this->getInvoicedToAttribute()) if (! $this->getInvoicedToAttribute())
return $this->getContractTermAttribute()*$this->billing_cost_normalised_taxed; return $this->contract_term*$this->billing_cost_normalised_taxed;
if ($this->getInvoicedToAttribute()->lessThan($max)) { if ($this->getInvoicedToAttribute()->lessThan($max)) {
$d = $this->getInvoicedToAttribute()->diffInDays($max); $d = $this->getInvoicedToAttribute()->diffInDays($max);
@ -1000,17 +988,6 @@ class Service extends Model implements IDs
->get(); ->get();
} }
/**
* Determine if a service is active. It is active, if active=1, or the order_status is not in self::INACTIVE_STATUS[]
*
* @return bool
*/
public function isActive(): bool
{
return $this->attributes['active']
|| ($this->order_status && (! in_array($this->order_status,self::INACTIVE_STATUS)));
}
/** /**
* Determine if the current user has the role for this service * Determine if the current user has the role for this service
* *
@ -1060,28 +1037,6 @@ class Service extends Model implements IDs
return FALSE; return FALSE;
} }
public function isContract(): bool
{
return $this->getContractEndAttribute() && $this->getContractEndAttribute()->greaterThan(Carbon::now());
}
/**
* Identify if a service is being ordered, ie: not active yet nor cancelled
*
* @return bool
*/
public function isPending(): bool
{
return (! $this->active)
&& (! is_null($this->order_status))
&& (! in_array($this->order_status,array_merge(self::INACTIVE_STATUS,['INACTIVE'])));
}
public function isPendingCancel(): bool
{
return in_array(strtolower($this->order_status),['cancel-request','cancel-pending']);
}
/** /**
* Generate a collection of invoice_item objects that will be billed for the next invoice * Generate a collection of invoice_item objects that will be billed for the next invoice
* *
@ -1091,7 +1046,7 @@ class Service extends Model implements IDs
*/ */
public function next_invoice_items(Carbon $billdate=NULL): Collection public function next_invoice_items(Carbon $billdate=NULL): Collection
{ {
if ($this->wasCancelled() || (! $this->is_billed)) if ($this->is_cancelled || (! $this->is_billed))
return collect(); return collect();
$o = collect(); $o = collect();
@ -1180,14 +1135,4 @@ class Service extends Model implements IDs
{ {
return $this->order_info ? $this->order_info->get($key) : NULL; return $this->order_info ? $this->order_info->get($key) : NULL;
} }
/**
* Service that was cancelled or never provisioned
*
* @return bool
*/
public function wasCancelled(): bool
{
return in_array($this->order_status,self::INACTIVE_STATUS);
}
} }

View File

@ -0,0 +1,7 @@
@if($change)
<div class="ribbon-wrapper ribbon-lg">
<div class="ribbon bg-warning">
Change
</div>
</div>
@endif

View File

@ -0,0 +1,7 @@
@if($pending)
<div class="ribbon-wrapper ribbon-lg">
<div class="ribbon bg-warning">
Pending
</div>
</div>
@endif

View File

@ -32,7 +32,7 @@
<div class="col-8 col-sm-5 col-md-12 col-lg-6"> <div class="col-8 col-sm-5 col-md-12 col-lg-6">
<x-leenooks::form.date name="change_date" icon="fa-calendar" label="Request Change Date" :value="Carbon::now()->addDays(7)->format('Y-m-d')"/> <x-leenooks::form.date name="change_date" icon="fa-calendar" label="Request Change Date" :value="Carbon::now()->addDays(7)->format('Y-m-d')"/>
</div> </div>
@if($so->isContract()) @if($so->is_contracted)
<div class="col-12 col-sm-7 col-md-12 col-lg-6"> <div class="col-12 col-sm-7 col-md-12 col-lg-6">
<strong>NOTE</strong>: This service is in a contract until <strong>{{ $so->contract_end->format('Y-m-d') }}</strong>, thus it may not be able to change plans. <strong>NOTE</strong>: This service is in a contract until <strong>{{ $so->contract_end->format('Y-m-d') }}</strong>, thus it may not be able to change plans.
</div> </div>

View File

@ -40,7 +40,7 @@
<li class="nav-item ml-auto"><a class="nav-link" href="#billing" data-toggle="tab">Billing History</a></li> <li class="nav-item ml-auto"><a class="nav-link" href="#billing" data-toggle="tab">Billing History</a></li>
<li class="nav-item"><a class="nav-link" href="#internal" data-toggle="tab">Internal</a></li> <li class="nav-item"><a class="nav-link" href="#internal" data-toggle="tab">Internal</a></li>
<li class="nav-item"><a @class(['nav-link','active'=>session()->has('service_update')]) href="#update" data-toggle="tab">Update</a></li> <li class="nav-item"><a @class(['nav-link','active'=>session()->has('service_update')]) href="#update" data-toggle="tab">Update</a></li>
@if($o->active || $o->isPending()) @if($o->active || $o->is_pending_active)
<li class="nav-item"><a @class(['nav-link','active'=>session()->has('charge_add')]) href="#charge" data-toggle="tab">Charge</a></li> <li class="nav-item"><a @class(['nav-link','active'=>session()->has('charge_add')]) href="#charge" data-toggle="tab">Charge</a></li>
@endif @endif
@endcan @endcan
@ -95,7 +95,7 @@
@include('theme.backend.adminlte.service.widget.update') @include('theme.backend.adminlte.service.widget.update')
</div> </div>
@if($o->active || $o->isPending()) @if($o->active || $o->is_pending_active)
<div @class(['tab-pane','fade','show active'=>session()->pull('charge_add')]) id="charge"> <div @class(['tab-pane','fade','show active'=>session()->pull('charge_add')]) id="charge">
@include('theme.backend.adminlte.service.widget.charge') @include('theme.backend.adminlte.service.widget.charge')
</div> </div>

View File

@ -1,13 +1,8 @@
<!-- $o=Service\Broadband::class --> <!-- $o=Service\Broadband::class -->
<div class="card"> <div class="card">
@if($o->service->isPending()) <x-ribbons.change :change="$o->service->is_pending_change"/>
<div class="ribbon-wrapper ribbon-lg"> <x-ribbons.pending :pending="$o->service->is_pending_active"/>
<div class="ribbon bg-warning">
Pending
</div>
</div>
@endif
<div class="card-header bg-gray-dark"> <div class="card-header bg-gray-dark">
<h3 class="card-title">Broadband Details</h3> <h3 class="card-title">Broadband Details</h3>

View File

@ -1,13 +1,8 @@
<!-- $o=Service\Domain::class --> <!-- $o=Service\Domain::class -->
<div class="card"> <div class="card">
@if($o->service->isPending()) <x-ribbons.change :change="$o->service->is_pending_change"/>
<div class="ribbon-wrapper ribbon-lg"> <x-ribbons.pending :pending="$o->service->is_pending_active"/>
<div class="ribbon bg-warning">
Pending
</div>
</div>
@endif
<div class="card-header bg-gray-dark"> <div class="card-header bg-gray-dark">
<h3 class="card-title">Domain Details</h3> <h3 class="card-title">Domain Details</h3>

View File

@ -1,13 +1,8 @@
<!-- $o=Service\Email::class --> <!-- $o=Service\Email::class -->
<div class="card"> <div class="card">
@if($o->service->isPending()) <x-ribbons.change :change="$o->service->is_pending_change"/>
<div class="ribbon-wrapper ribbon-lg"> <x-ribbons.pending :pending="$o->service->is_pending_active"/>
<div class="ribbon bg-warning">
Pending
</div>
</div>
@endif
<div class="card-header bg-gray-dark"> <div class="card-header bg-gray-dark">
<h3 class="card-title">Email Hosting Details</h3> <h3 class="card-title">Email Hosting Details</h3>

View File

@ -1,13 +1,8 @@
<!-- $o=Service\Host::class --> <!-- $o=Service\Host::class -->
<div class="card"> <div class="card">
@if($o->service->isPending()) <x-ribbons.change :change="$o->service->is_pending_change"/>
<div class="ribbon-wrapper ribbon-lg"> <x-ribbons.pending :pending="$o->service->is_pending_active"/>
<div class="ribbon bg-warning">
Pending
</div>
</div>
@endif
<div class="card-header bg-gray-dark"> <div class="card-header bg-gray-dark">
<h3 class="card-title">Hosting Details</h3> <h3 class="card-title">Hosting Details</h3>

View File

@ -22,7 +22,7 @@
<th>Status</th> <th>Status</th>
<td><x-button.status :status="$o->status" :substatus="$o->order_status"/></td> <td><x-button.status :status="$o->status" :substatus="$o->order_status"/></td>
</tr> </tr>
@if($o->isPendingCancel()) @if($o->is_pending_cancel)
<tr> <tr>
<th>Cancel Date</th> <th>Cancel Date</th>
<td>{{ $o->stop_at->format('Y-m-d') }}</td> <td>{{ $o->stop_at->format('Y-m-d') }}</td>
@ -34,13 +34,13 @@
<td>{{ $o->order_info_reference ?? '' }}</td> <td>{{ $o->order_info_reference ?? '' }}</td>
</tr> </tr>
@endif @endif
@if($o->start_at && $o->isPending()) @if($o->start_at && $o->is_pending_active)
<tr> <tr>
<th>Pending Connection</th> <th>Pending Connection</th>
<td>{{ $o->start_at->format('Y-m-d') }}</td> <td>{{ $o->start_at->format('Y-m-d') }}</td>
</tr> </tr>
@endif @endif
@if(($o->active || $o->isPending()) && (! $o->external_billing)) @if(($o->active || $o->is_pending_active) && (! $o->external_billing))
<tr> <tr>
<th>Billed</th> <th>Billed</th>
<td>{{ $o->billing_interval_name }}</td> <td>{{ $o->billing_interval_name }}</td>
@ -53,7 +53,7 @@
<td>${{ number_format($o->billing_charge_taxed,2) }}</td> <td>${{ number_format($o->billing_charge_taxed,2) }}</td>
@endif @endif
</tr> </tr>
@if($o->isActive() && $o->invoiced_to) @if($o->is_active && $o->invoiced_to)
<tr> <tr>
<th>Invoiced To</th> <th>Invoiced To</th>
<td>{{ $o->invoiced_to->format('Y-m-d') }}</td> <td>{{ $o->invoiced_to->format('Y-m-d') }}</td>
@ -83,7 +83,7 @@
<td>@if($o->billing)Direct Debit @else Invoice @endif</td> <td>@if($o->billing)Direct Debit @else Invoice @endif</td>
</tr> </tr>
@elseif($o->wasCancelled()) @elseif($o->is_cancelled)
<tr> <tr>
<th>Cancelled</th> <th>Cancelled</th>
<td>{{ ($o->stop_at ?: $o->paid_to)?->format('Y-m-d') }}</td> <td>{{ ($o->stop_at ?: $o->paid_to)?->format('Y-m-d') }}</td>
@ -92,7 +92,7 @@
</table> </table>
</div> </div>
@if($o->active || $o->isPending()) @if($o->active || $o->is_pending_active)
<div class="card-footer sm"> <div class="card-footer sm">
<strong><sup>*</sup>NOTE:</strong> Estimated Invoice does not include any setup, connection nor all current billing cycle usage charges. <strong><sup>*</sup>NOTE:</strong> Estimated Invoice does not include any setup, connection nor all current billing cycle usage charges.
</div> </div>

View File

@ -131,7 +131,7 @@
<tr> <tr>
<th>Contract Left</th> <th>Contract Left</th>
@if($o->isContract()) @if($o->is_contracted)
<td>${{ number_format($o->billing_charge_to($o->contract_end),2) }} (<small>{{ $o->paid_to?->format('Y-m-d') }}</small>)</td> <td>${{ number_format($o->billing_charge_to($o->contract_end),2) }} (<small>{{ $o->paid_to?->format('Y-m-d') }}</small>)</td>
<td>${{ number_format($o->billing_cost_to($o->contract_end),2) }} (<small>{{ $o->invoiced_to?->format('Y-m-d') }}</small>)</td> <td>${{ number_format($o->billing_cost_to($o->contract_end),2) }} (<small>{{ $o->invoiced_to?->format('Y-m-d') }}</small>)</td>
<td>{{ $o->contract_end->format('Y-m-d') }}<br><small>({{ $o->contract_end->diffForHumans(now(),CarbonInterface::DIFF_RELATIVE_TO_OTHER,FALSE,2) }} today)</small></td> <td>{{ $o->contract_end->format('Y-m-d') }}<br><small>({{ $o->contract_end->diffForHumans(now(),CarbonInterface::DIFF_RELATIVE_TO_OTHER,FALSE,2) }} today)</small></td>

View File

@ -1,13 +1,8 @@
<!-- $o=Service\Phone::class --> <!-- $o=Service\Phone::class -->
<div class="card"> <div class="card">
@if($o->service->isPending()) <x-ribbons.change :change="$o->service->is_pending_change"/>
<div class="ribbon-wrapper ribbon-lg"> <x-ribbons.pending :pending="$o->service->is_pending_active"/>
<div class="ribbon bg-warning">
Pending
</div>
</div>
@endif
<div class="card-header bg-gray-dark"> <div class="card-header bg-gray-dark">
<h3 class="card-title">Phone Details</h3> <h3 class="card-title">Phone Details</h3>

View File

@ -1,11 +1,6 @@
<div class="card"> <div class="card">
@if($o->service->isPending()) <x-ribbons.change :change="$o->service->is_pending_change"/>
<div class="ribbon-wrapper ribbon-lg"> <x-ribbons.pending :pending="$o->service->is_pending_active"/>
<div class="ribbon bg-warning">
Pending
</div>
</div>
@endif
<div class="card-header bg-gray-dark"> <div class="card-header bg-gray-dark">
<h3 class="card-title">SSL Details</h3> <h3 class="card-title">SSL Details</h3>