Introduce cost and cost override into service model. Update service internal view to show cost/billing left on contracted services

This commit is contained in:
Deon George 2025-05-16 11:10:28 +10:00
parent 48d7968d0c
commit 4c24f09195
2 changed files with 93 additions and 10 deletions

View File

@ -45,6 +45,7 @@ use App\Traits\{ScopeAccountUserAuthorised,ScopeServiceActive,SiteID};
*
* Methods:
* + isChargeOverridden : Has the price been overridden?
* + isCostOverridden : Has the cost been overridden?
* + isPending : Is this a pending active service
*
* @package App\Models
@ -535,6 +536,11 @@ class Service extends Model implements IDs
return $this->account->taxed($this->billing_charge());
}
public function getBillingCostAttribute(): float
{
return $this->account->taxed($this->billing_cost());
}
/**
* Determine a monthly price for a service, even if it is billed at a different frequency
*
@ -543,7 +549,16 @@ class Service extends Model implements IDs
*/
public function getBillingChargeNormalisedAttribute(): float
{
return number_format($this->getBillingChargeAttribute()*Invoice::billing_change($this->getBillingIntervalAttribute(),$this->offering->billing_interval),2);
return number_format(
$this->getBillingChargeAttribute()*Invoice::billing_change($this->getBillingIntervalAttribute(),$this->offering->billing_interval),
2);
}
public function getBillingCostNormalisedAttribute(): float
{
return number_format(
$this->getBillingCostAttribute()*Invoice::billing_change($this->getBillingIntervalAttribute(),$this->offering->billing_interval),
2);
}
/**
@ -567,7 +582,7 @@ class Service extends Model implements IDs
}
/**
* Return the earliest date that the service can be cancelled
* Return the earliest date that the service can be canceled as per contract/billing intervals
*
* @return Carbon
*/
@ -916,17 +931,21 @@ class Service extends Model implements IDs
/**
* Provide billing charge to a future date
*
* If $date is earlier than our contract end date, then our charge is to the contract end date.
* If $date is after our contract end date:
* + and we are still in contract, then our charge is to contract end date plus additional time, else
* + then our charge is the months to the $date
*
* @param Carbon $date
* @return float
* @throws Exception
*/
public function billing_charge_to(Carbon $date): float
{
// if the date is less than the paid to, but less than the cancel date to, return cancel-paid to charge
// If the date is greater than the paid to, and less than the cancel date to, return cancel-paid to charge
if ($this->getPaidToAttribute()->lessThan($this->getCancelDateAttribute())) {
$max = max($date,$this->getPaidToAttribute())->clone();
$d = $max->diffInDays($this->getCancelDateAttribute());
$max = max($date,$this->getCancelDateAttribute())->clone();
if ($this->getPaidToAttribute()->lessThan($max)) {
$d = $this->getPaidToAttribute()->diffInDays($max);
return $this->account->taxed($d/30*$this->getBillingChargeNormalisedAttribute());
}
@ -934,6 +953,42 @@ class Service extends Model implements IDs
return 0;
}
/**
* The amount we are charged for the client to have this service
* @return float
*/
public function billing_cost(): float
{
return is_null($this->cost)
? $this->product->getBaseCostAttribute()
: $this->cost;
}
/**
* Calculate our costs to keep this service to a future date
*
* If $date is earlier than the contract end date, then our cost is to the contract end date.
* If $date is after the contract end date:
* + and we are still in contract, then our cost is to contract end date plus additional time, else
* + then our cost is the months to the $date
*
* @param Carbon $date
* @return float
* @throws Exception
*/
public function billing_cost_to(Carbon $date): float
{
$max = max($date,$this->getCancelDateAttribute())->clone();
if ($this->getInvoicedToAttribute()->lessThan($max)) {
$d = $this->getInvoicedToAttribute()->diffInDays($max);
return $this->account->taxed($d/30*$this->getBillingCostNormalisedAttribute());
}
return 0;
}
/**
* Get the stage parameters
*
@ -1065,6 +1120,11 @@ class Service extends Model implements IDs
return ! is_null($this->price);
}
public function isCostOverridden(): bool
{
return ! is_null($this->cost);
}
public function isContract(): bool
{
return $this->getContractEndAttribute()->greaterThan(Carbon::now());

View File

@ -1,4 +1,6 @@
@use(App\Models\Invoice)
@use(Carbon\CarbonInterface)
@php($c=$o->product)
<!-- $o=Service::class, $p=Product::class -->
@ -29,7 +31,8 @@
<tbody>
<tr>
<th>Product</th>
<td class="text-center" colspan="2"><a href="{{ url('a/product/details',$c->id) }}">#{{ $c->supplied->id }}: {{ $c->supplied->name_long }}</a></td>
<td><a href="{{ url('a/product/details',$o->product_id) }}">#{{ $o->product_id }}: {{ $o->product->name }}</a></td>
<td><a href="{{ url('a/product/details',$c->id) }}">#{{ $c->supplied->id }}: {{ $c->supplied->name_long }}</a></td>
@if($p->exists)
<th>&nbsp;</th>
<td class="text-center" colspan="2">#{{ $p->supplied->id }}: {{ $p->supplied->name_long }}</td>
@ -79,10 +82,16 @@
@if($x)
<abbr title="${{ number_format($b=$o->account->taxed($c->base_charge)*Invoice::billing_change($o->billing_interval,Invoice::BILL_MONTHLY),2) }}">${{ number_format($b=$o->billing_charge_normalised,2) }}
@else
{{ number_format($b=$o->billing_charge_normalised,2) }}
${{ number_format($b=$o->billing_charge_normalised,2) }}
@endif
</td>
<td @if($x=$o->isCostOverridden()) class="text-danger" @endif>
@if($x)
<abbr title="${{ number_format($a=$o->account->taxed($c->base_cost)*Invoice::billing_change($o->billing_interval,Invoice::BILL_MONTHLY),2) }}">${{ number_format($b=$o->billing_cost_normalised,2) }}
@else
${{ number_format($o->billing_cost_normalised,2) }}
@endif
</td>
<td>${{ number_format($a=$o->account->taxed($c->base_cost)*Invoice::billing_change($o->billing_interval,Invoice::BILL_MONTHLY),2) }}</td>
<td>{!! markup($a,$b) !!}</td>
@if($p->exists)
<td @if($x=$o->isChargeOverridden()) class="text-danger" @endif>${{ number_format($b=$o->account->taxed($p->base_charge)*Invoice::billing_change($o->billing_interval,Invoice::BILL_MONTHLY),2) }}</td>
@ -109,11 +118,25 @@
<td>${{ number_format($b=$o->account->taxed($o->product->min_charge),2) }}</td>
<td>${{ number_format($a=$o->account->taxed($c->supplied->min_cost),2) }}</td>
<td>{!! markup($a,$b) !!}</td>
@if($p->exists)
<td>${{ number_format($a=$o->account->taxed($p->min_charge),2) }}</td>
<td>${{ number_format($a=$o->account->taxed($p->supplied->min_cost ?? 0),2) }}</td>
<td>{!! markup($a,$b) !!}</td>
@endif
</tr>
<tr>
<th>Contract Left</th>
@if($o->isContract())
<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>{{ $o->contract_end->format('Y-m-d') }}<br><small>({{ $o->contract_end->diffForHumans(now(),CarbonInterface::DIFF_RELATIVE_TO_OTHER,FALSE,2) }} today)</small></td>
@else
<td colspan="2" class="text-center">Not on contract</td>
<td>&nbsp;</td>
@endif
</tr>
</tbody>
</table>