Invoice testing and line item catchup
This commit is contained in:
parent
f41fc3eb9c
commit
5cc0dcd8e1
@ -14,12 +14,18 @@ class Product extends Model
|
|||||||
const RECORD_ID = 'product';
|
const RECORD_ID = 'product';
|
||||||
public $incrementing = FALSE;
|
public $incrementing = FALSE;
|
||||||
|
|
||||||
protected $table = 'ab_product';
|
|
||||||
protected $with = ['descriptions'];
|
|
||||||
|
|
||||||
const CREATED_AT = 'date_orig';
|
const CREATED_AT = 'date_orig';
|
||||||
const UPDATED_AT = 'date_last';
|
const UPDATED_AT = 'date_last';
|
||||||
|
|
||||||
public $dateFormat = 'U';
|
public $dateFormat = 'U';
|
||||||
|
protected $table = 'ab_product';
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
// @todo convert existing data to a json array
|
||||||
|
// 'price_group'=>'array',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $with = ['descriptions'];
|
||||||
|
|
||||||
public function descriptions()
|
public function descriptions()
|
||||||
{
|
{
|
||||||
@ -126,7 +132,7 @@ class Product extends Model
|
|||||||
|
|
||||||
public function getPriceArrayAttribute()
|
public function getPriceArrayAttribute()
|
||||||
{
|
{
|
||||||
return unserialize($this->price_group);
|
return unserialize($this->attributes['price_group']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getPriceTypeAttribute()
|
public function getPriceTypeAttribute()
|
||||||
|
@ -43,7 +43,7 @@ class Service extends Model
|
|||||||
|
|
||||||
protected $dates = [
|
protected $dates = [
|
||||||
'date_last_invoice',
|
'date_last_invoice',
|
||||||
'date_next_invoice'.
|
'date_next_invoice',
|
||||||
'date_start',
|
'date_start',
|
||||||
'date_end',
|
'date_end',
|
||||||
];
|
];
|
||||||
@ -324,15 +324,17 @@ class Service extends Model
|
|||||||
/**
|
/**
|
||||||
* Return the date for the next invoice
|
* Return the date for the next invoice
|
||||||
*
|
*
|
||||||
* @todo This function negates the need for date_next_invoice
|
* @todo Change date_next_invoice to connect_date/invoice_start_date
|
||||||
* @return Carbon|string
|
* @return Carbon|string
|
||||||
*/
|
*/
|
||||||
public function getInvoiceNextAttribute()
|
public function getInvoiceNextAttribute()
|
||||||
{
|
{
|
||||||
$last = $this->getInvoiceToAttribute();
|
$last = $this->getInvoiceToAttribute();
|
||||||
$date = $last ? $last->addDay() : Carbon::now();
|
$date = $last
|
||||||
|
? $last->addDay()
|
||||||
|
: ($this->date_next_invoice ? $this->date_next_invoice->clone() : Carbon::now());
|
||||||
|
|
||||||
return request()->wantsJson() ? $date->format('Y-m-d') : $date;
|
return $date;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -450,7 +452,7 @@ class Service extends Model
|
|||||||
*/
|
*/
|
||||||
public function getInvoiceToAttribute()
|
public function getInvoiceToAttribute()
|
||||||
{
|
{
|
||||||
return $this->invoice_items->count() ? $this->invoice_items->last()->date_stop : NULL;
|
return ($x=$this->invoice_items->filter(function($item) { return $item->item_type === 0;}))->count() ? $x->last()->date_stop : NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getNameAttribute(): string
|
public function getNameAttribute(): string
|
||||||
@ -590,7 +592,9 @@ class Service extends Model
|
|||||||
*/
|
*/
|
||||||
public function getSDescAttribute(): string
|
public function getSDescAttribute(): string
|
||||||
{
|
{
|
||||||
return $this->type->service_description ?: 'Service Description NOT Defined for :'.$this->type->type;
|
return ($this->type AND $this->type->service_description)
|
||||||
|
? $this->type->service_description
|
||||||
|
: 'Service Description NOT Defined for :'.($this->type ? $this->type->type : $this->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -605,7 +609,9 @@ class Service extends Model
|
|||||||
*/
|
*/
|
||||||
public function getSNameAttribute(): string
|
public function getSNameAttribute(): string
|
||||||
{
|
{
|
||||||
return $this->type->service_name ?: 'Service Name NOT Defined for :'.$this->type->type;
|
return ($this->type AND $this->type->service_name)
|
||||||
|
? $this->type->service_name
|
||||||
|
: 'Service Name NOT Defined for :'.($this->type ? $this->type->type : $this->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -770,14 +776,36 @@ class Service extends Model
|
|||||||
* @return Collection
|
* @return Collection
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
public function next_invoice_items(): Collection
|
public function next_invoice_items(bool $future): Collection
|
||||||
{
|
{
|
||||||
$result = collect();
|
if ($this->wasCancelled())
|
||||||
|
return collect();
|
||||||
|
|
||||||
|
// 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)))
|
||||||
|
{
|
||||||
$o = new InvoiceItem;
|
$o = 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->price ?: $this->product->price($this->recur_schedule,'price_setup'); // @todo change to a method in this class
|
||||||
|
//$o->recurring_schedule = $this->recur_schedule;
|
||||||
|
$o->date_start = $this->invoice_next;
|
||||||
|
$o->date_stop = $this->invoice_next;
|
||||||
|
$o->quantity = 1;
|
||||||
|
|
||||||
|
$o->addTaxes($this->account->country->taxes);
|
||||||
|
$this->invoice_items->push($o);
|
||||||
|
}
|
||||||
|
|
||||||
// If the service is active, there will be service charges
|
// If the service is active, there will be service charges
|
||||||
if ($this->active or $this->isPending()) {
|
if ($this->active OR $this->isPending()) {
|
||||||
|
do {
|
||||||
|
$o = new InvoiceItem;
|
||||||
$o->active = TRUE;
|
$o->active = TRUE;
|
||||||
$o->service_id = $this->id;
|
$o->service_id = $this->id;
|
||||||
$o->product_id = $this->product_id;
|
$o->product_id = $this->product_id;
|
||||||
@ -789,24 +817,8 @@ class Service extends Model
|
|||||||
$o->quantity = $this->invoice_next_quantity;
|
$o->quantity = $this->invoice_next_quantity;
|
||||||
|
|
||||||
$o->addTaxes($this->account->country->taxes);
|
$o->addTaxes($this->account->country->taxes);
|
||||||
$result->push($o);
|
$this->invoice_items->push($o);
|
||||||
}
|
} while ($future == FALSE AND ($this->invoice_to < Carbon::now()->addDays(30)));
|
||||||
|
|
||||||
// If pending, add any connection charges
|
|
||||||
if ($this->isPending()) {
|
|
||||||
$o = new InvoiceItem;
|
|
||||||
$o->active = TRUE;
|
|
||||||
$o->service_id = $this->id;
|
|
||||||
$o->product_id = $this->product_id;
|
|
||||||
$o->item_type = 4;
|
|
||||||
$o->price_base = $this->price ?: $this->product->price($this->recur_schedule,'price_setup'); // @todo change to a method in this class
|
|
||||||
//$o->recurring_schedule = $this->recur_schedule;
|
|
||||||
$o->date_start = $this->invoice_next;
|
|
||||||
$o->date_stop = $this->invoice_next;
|
|
||||||
$o->quantity = 1;
|
|
||||||
|
|
||||||
$o->addTaxes($this->account->country->taxes);
|
|
||||||
$result->push($o);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add additional charges
|
// Add additional charges
|
||||||
@ -824,10 +836,10 @@ class Service extends Model
|
|||||||
$o->module_ref = $oo->id;
|
$o->module_ref = $oo->id;
|
||||||
|
|
||||||
$o->addTaxes($this->account->country->taxes);
|
$o->addTaxes($this->account->country->taxes);
|
||||||
$result->push($o);
|
$this->invoice_items->push($o);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return $this->invoice_items->filter(function($item) { return ! $item->exists; });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -887,4 +899,14 @@ class Service extends Model
|
|||||||
{
|
{
|
||||||
return $this->testNextStatusValid($status);
|
return $this->testNextStatusValid($status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service that was cancelled or never provisioned
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function wasCancelled(): bool
|
||||||
|
{
|
||||||
|
return in_array($this->order_status,$this->inactive_status);
|
||||||
|
}
|
||||||
}
|
}
|
@ -491,7 +491,7 @@ class User extends Authenticatable
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($o->next_invoice_items() as $oo)
|
foreach ($o->next_invoice_items($future) as $oo)
|
||||||
$result->push($oo);
|
$result->push($oo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,65 +13,84 @@ $factory->define(App\Models\InvoiceItem::class, function (Faker $faker) {
|
|||||||
$factory->state(App\Models\InvoiceItem::class,'week',[
|
$factory->state(App\Models\InvoiceItem::class,'week',[
|
||||||
'date_start'=>Carbon::now()->startOfWeek(),
|
'date_start'=>Carbon::now()->startOfWeek(),
|
||||||
'date_stop'=>Carbon::now()->endOfWeek(),
|
'date_stop'=>Carbon::now()->endOfWeek(),
|
||||||
|
'item_type'=>0,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$factory->state(App\Models\InvoiceItem::class,'week-mid',[
|
$factory->state(App\Models\InvoiceItem::class,'week-mid',[
|
||||||
'date_start'=>Carbon::now()->startOfWeek(),
|
'date_start'=>Carbon::now()->startOfWeek(),
|
||||||
'date_stop'=>Carbon::now()->endOfWeek()->addDays(3),
|
'date_stop'=>Carbon::now()->endOfWeek()->addDays(3),
|
||||||
|
'item_type'=>0,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Monthly
|
// Monthly
|
||||||
$factory->state(App\Models\InvoiceItem::class,'month',[
|
$factory->state(App\Models\InvoiceItem::class,'month',[
|
||||||
'date_start'=>Carbon::now()->startOfMonth(),
|
'date_start'=>Carbon::now()->startOfMonth(),
|
||||||
'date_stop'=>Carbon::now()->endOfMonth(),
|
'date_stop'=>Carbon::now()->endOfMonth(),
|
||||||
|
'item_type'=>0,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$factory->state(App\Models\InvoiceItem::class,'month-mid',[
|
$factory->state(App\Models\InvoiceItem::class,'month-mid',[
|
||||||
'date_start'=>Carbon::now()->startOfMonth(),
|
'date_start'=>Carbon::now()->startOfMonth(),
|
||||||
'date_stop'=>Carbon::now()->endOfMonth()->addDays(Carbon::now()->daysInMonth/2+1),
|
'date_stop'=>Carbon::now()->endOfMonth()->addDays(Carbon::now()->daysInMonth/2+1),
|
||||||
|
'item_type'=>0,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Quarterly
|
// Quarterly
|
||||||
$factory->state(App\Models\InvoiceItem::class,'quarter',[
|
$factory->state(App\Models\InvoiceItem::class,'quarter',[
|
||||||
'date_start'=>Carbon::now()->startOfQuarter(),
|
'date_start'=>Carbon::now()->startOfQuarter(),
|
||||||
'date_stop'=>Carbon::now()->endOfQuarter(),
|
'date_stop'=>Carbon::now()->endOfQuarter(),
|
||||||
|
'item_type'=>0,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$factory->state(App\Models\InvoiceItem::class,'quarter-mid',[
|
$factory->state(App\Models\InvoiceItem::class,'quarter-mid',[
|
||||||
'date_start'=>Carbon::now()->startOfQuarter(),
|
'date_start'=>Carbon::now()->startOfQuarter(),
|
||||||
'date_stop'=>Carbon::now()->startOfQuarter()->addDays(45),
|
'date_stop'=>Carbon::now()->startOfQuarter()->addDays(45),
|
||||||
|
'item_type'=>0,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Half Yearly
|
// Half Yearly
|
||||||
$factory->state(App\Models\InvoiceItem::class,'half',[
|
$factory->state(App\Models\InvoiceItem::class,'half',[
|
||||||
'date_start'=>Carbon::now()->startOfHalf(),
|
'date_start'=>Carbon::now()->startOfHalf(),
|
||||||
'date_stop'=>Carbon::now()->endOfHalf(),
|
'date_stop'=>Carbon::now()->endOfHalf(),
|
||||||
|
'item_type'=>0,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$factory->state(App\Models\InvoiceItem::class,'half-mid',[
|
$factory->state(App\Models\InvoiceItem::class,'half-mid',[
|
||||||
'date_start'=>Carbon::now()->startOfHalf(),
|
'date_start'=>Carbon::now()->startOfHalf(),
|
||||||
'date_stop'=>Carbon::now()->startOfHalf()->addDays(90),
|
'date_stop'=>Carbon::now()->startOfHalf()->addDays(90),
|
||||||
|
'item_type'=>0,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Yearly
|
// Yearly
|
||||||
$factory->state(App\Models\InvoiceItem::class,'year',[
|
$factory->state(App\Models\InvoiceItem::class,'year',[
|
||||||
'date_start'=>Carbon::now()->startOfYear(),
|
'date_start'=>Carbon::now()->startOfYear(),
|
||||||
'date_stop'=>Carbon::now()->endOfYear(),
|
'date_stop'=>Carbon::now()->endOfYear(),
|
||||||
|
'item_type'=>0,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$factory->state(App\Models\InvoiceItem::class,'year-mid',[
|
$factory->state(App\Models\InvoiceItem::class,'year-mid',[
|
||||||
'date_start'=>Carbon::now()->startOfYear(),
|
'date_start'=>Carbon::now()->startOfYear(),
|
||||||
'date_stop'=>Carbon::now()->startOfYear()->addDays(181),
|
'date_stop'=>Carbon::now()->startOfYear()->addDays(181),
|
||||||
|
'item_type'=>0,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Two Yearly (price_recurr_strict ignored)
|
// Two Yearly (price_recurr_strict ignored)
|
||||||
$factory->state(App\Models\InvoiceItem::class,'2year',[
|
$factory->state(App\Models\InvoiceItem::class,'2year',[
|
||||||
'date_start'=>Carbon::now()->subyear(),
|
'date_start'=>Carbon::now()->subyear(),
|
||||||
'date_stop'=>Carbon::now()->subday(),
|
'date_stop'=>Carbon::now()->subday(),
|
||||||
|
'item_type'=>0,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Three Yearly (price_recurr_strict ignored)
|
// Three Yearly (price_recurr_strict ignored)
|
||||||
$factory->state(App\Models\InvoiceItem::class,'3year',[
|
$factory->state(App\Models\InvoiceItem::class,'3year',[
|
||||||
'date_start'=>Carbon::now()->subyear(2),
|
'date_start'=>Carbon::now()->subyear(2),
|
||||||
'date_stop'=>Carbon::now()->subday(),
|
'date_stop'=>Carbon::now()->subday(),
|
||||||
|
'item_type'=>0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Last Month
|
||||||
|
$factory->state(App\Models\InvoiceItem::class,'next-invoice',[
|
||||||
|
'date_start'=>Carbon::now()->startOfMonth(),
|
||||||
|
'date_stop'=>Carbon::now()->endOfMonth(),
|
||||||
|
'item_type'=>0,
|
||||||
]);
|
]);
|
@ -5,15 +5,25 @@ use Faker\Generator as Faker;
|
|||||||
$factory->define(App\Models\Product::class, function (Faker $faker) {
|
$factory->define(App\Models\Product::class, function (Faker $faker) {
|
||||||
return [
|
return [
|
||||||
'id'=>1,
|
'id'=>1,
|
||||||
|
'site_id'=>1,
|
||||||
|
'taxable'=>1,
|
||||||
|
'price_group'=>serialize([
|
||||||
|
1=>[
|
||||||
|
0=>[
|
||||||
|
'price_setup'=>50,
|
||||||
|
'price_base'=>100,
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]),
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
$factory->state(App\Models\Product::class,'active',[
|
$factory->state(App\Models\Product::class,'active',[
|
||||||
'active' => '1',
|
'active' => 1,
|
||||||
]);
|
]);
|
||||||
$factory->state(App\Models\Product::class,'strict',[
|
$factory->state(App\Models\Product::class,'strict',[
|
||||||
'price_recurr_strict' => '1',
|
'price_recurr_strict' => 1,
|
||||||
]);
|
]);
|
||||||
$factory->state(App\Models\Product::class,'notstrict',[
|
$factory->state(App\Models\Product::class,'notstrict',[
|
||||||
'price_recurr_strict' => '0',
|
'price_recurr_strict' => 0,
|
||||||
]);
|
]);
|
@ -5,6 +5,7 @@ use Faker\Generator as Faker;
|
|||||||
$factory->define(App\Models\Service::class, function (Faker $faker) {
|
$factory->define(App\Models\Service::class, function (Faker $faker) {
|
||||||
return [
|
return [
|
||||||
'account_id'=>1,
|
'account_id'=>1,
|
||||||
|
'active'=>1,
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -93,3 +94,16 @@ $factory->afterMakingState(App\Models\Service::class,'3year',function ($service,
|
|||||||
$service->setRelation('invoice_items',$invoice_items);
|
$service->setRelation('invoice_items',$invoice_items);
|
||||||
$service->recur_schedule = 6;
|
$service->recur_schedule = 6;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Last Month
|
||||||
|
$factory->afterMakingState(App\Models\Service::class,'next-invoice',function ($service,$faker) {
|
||||||
|
$invoice_items = factory(App\Models\InvoiceItem::class,1)->state('next-invoice')->make();
|
||||||
|
$service->setRelation('invoice_items',$invoice_items);
|
||||||
|
$service->recur_schedule = 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
// New Connect
|
||||||
|
$factory->afterMakingState(App\Models\Service::class,'new-connect',function ($service,$faker) {
|
||||||
|
$service->recur_schedule = 1;
|
||||||
|
$service->date_next_invoice = \Carbon\Carbon::now()->subMonth()->startOfMonth()->addDays(\Carbon\Carbon::now()->subMonth()->daysInMonth/2);
|
||||||
|
});
|
@ -41,7 +41,7 @@
|
|||||||
<td>@if ($o->autopay)Direct Debit @else Invoice @endif</td>
|
<td>@if ($o->autopay)Direct Debit @else Invoice @endif</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
@else
|
@elseif(! $o->wasCancelled())
|
||||||
<tr>
|
<tr>
|
||||||
<th>Cancelled</th>
|
<th>Cancelled</th>
|
||||||
<td>{!! $o->date_end ? $o->date_end->format('Y-m-d') : $o->paid_to->format('Y-m-d').'<sup>*</sup>' !!}</td>
|
<td>{!! $o->date_end ? $o->date_end->format('Y-m-d') : $o->paid_to->format('Y-m-d').'<sup>*</sup>' !!}</td>
|
||||||
|
33
tests/Feature/InvoiceTest.php
Normal file
33
tests/Feature/InvoiceTest.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\Product;
|
||||||
|
use App\Models\Service;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Foundation\Testing\WithFaker;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class InvoiceTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* A basic feature test example.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testInvoiceAmount()
|
||||||
|
{
|
||||||
|
// Create two services for the same account
|
||||||
|
|
||||||
|
// First service was billed a month ago, so this invoice will have 1 service charge
|
||||||
|
$o = factory(Service::class)->states('next-invoice')->make();
|
||||||
|
$o->setRelation('product',factory(Product::class)->states('strict')->make());
|
||||||
|
$this->assertEquals(110,$o->next_invoice_items(FALSE)->sum('total'),'Invoice Equals 110');
|
||||||
|
|
||||||
|
// Second service wasnt billed, connected 1.5 months ago, so invoice will have 2 service charges - 1 x 0.5 month and 1 x full month. and a connection charge
|
||||||
|
$o = factory(Service::class)->states('new-connect')->make();
|
||||||
|
$o->setRelation('product',factory(Product::class)->states('strict')->make());
|
||||||
|
$this->assertEqualsWithDelta(110+110+55+55,$o->next_invoice_items(FALSE)->sum('total'),2.5,'Invoice Equals 220');
|
||||||
|
}
|
||||||
|
}
|
@ -12,7 +12,7 @@ use Tests\TestCase;
|
|||||||
class ServiceTest extends TestCase
|
class ServiceTest extends TestCase
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* A basic feature test example.
|
* Test billing calculations
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
|
Loading…
Reference in New Issue
Block a user