diff --git a/app/Models/Product.php b/app/Models/Product.php index 821bf83..70c6b0d 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -14,12 +14,18 @@ class Product extends Model const RECORD_ID = 'product'; public $incrementing = FALSE; - protected $table = 'ab_product'; - protected $with = ['descriptions']; - const CREATED_AT = 'date_orig'; const UPDATED_AT = 'date_last'; + 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() { @@ -126,7 +132,7 @@ class Product extends Model public function getPriceArrayAttribute() { - return unserialize($this->price_group); + return unserialize($this->attributes['price_group']); } public function getPriceTypeAttribute() diff --git a/app/Models/Service.php b/app/Models/Service.php index 79159aa..1462548 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -43,7 +43,7 @@ class Service extends Model protected $dates = [ 'date_last_invoice', - 'date_next_invoice'. + 'date_next_invoice', 'date_start', 'date_end', ]; @@ -324,15 +324,17 @@ class Service extends Model /** * 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 */ public function getInvoiceNextAttribute() { $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() { - 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 @@ -590,7 +592,9 @@ class Service extends Model */ 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 { - 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,35 +776,22 @@ class Service extends Model * @return Collection * @throws Exception */ - public function next_invoice_items(): Collection + public function next_invoice_items(bool $future): Collection { - $result = collect(); - - $o = new InvoiceItem; - - // If the service is active, there will be service charges - if ($this->active or $this->isPending()) { - $o->active = TRUE; - $o->service_id = $this->id; - $o->product_id = $this->product_id; - $o->item_type = 0; - $o->price_base = $this->price ?: $this->product->price($this->recur_schedule); // @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_end; - $o->quantity = $this->invoice_next_quantity; - - $o->addTaxes($this->account->country->taxes); - $result->push($o); - } + if ($this->wasCancelled()) + return collect(); // If pending, add any connection charges - if ($this->isPending()) { + // 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->active = TRUE; $o->service_id = $this->id; $o->product_id = $this->product_id; - $o->item_type = 4; + $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; @@ -806,7 +799,26 @@ class Service extends Model $o->quantity = 1; $o->addTaxes($this->account->country->taxes); - $result->push($o); + $this->invoice_items->push($o); + } + + // If the service is active, there will be service charges + if ($this->active OR $this->isPending()) { + 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 = $this->price ?: $this->product->price($this->recur_schedule); // @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_end; + $o->quantity = $this->invoice_next_quantity; + + $o->addTaxes($this->account->country->taxes); + $this->invoice_items->push($o); + } while ($future == FALSE AND ($this->invoice_to < Carbon::now()->addDays(30))); } // Add additional charges @@ -824,10 +836,10 @@ class Service extends Model $o->module_ref = $oo->id; $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); } + + /** + * Service that was cancelled or never provisioned + * + * @return bool + */ + public function wasCancelled(): bool + { + return in_array($this->order_status,$this->inactive_status); + } } \ No newline at end of file diff --git a/app/User.php b/app/User.php index 23192f1..a027946 100644 --- a/app/User.php +++ b/app/User.php @@ -491,7 +491,7 @@ class User extends Authenticatable continue; } - foreach ($o->next_invoice_items() as $oo) + foreach ($o->next_invoice_items($future) as $oo) $result->push($oo); } diff --git a/database/factories/InvoiceItemFactory.php b/database/factories/InvoiceItemFactory.php index b79e84c..f67296f 100644 --- a/database/factories/InvoiceItemFactory.php +++ b/database/factories/InvoiceItemFactory.php @@ -13,65 +13,84 @@ $factory->define(App\Models\InvoiceItem::class, function (Faker $faker) { $factory->state(App\Models\InvoiceItem::class,'week',[ 'date_start'=>Carbon::now()->startOfWeek(), 'date_stop'=>Carbon::now()->endOfWeek(), + 'item_type'=>0, ]); $factory->state(App\Models\InvoiceItem::class,'week-mid',[ 'date_start'=>Carbon::now()->startOfWeek(), 'date_stop'=>Carbon::now()->endOfWeek()->addDays(3), + 'item_type'=>0, ]); // Monthly $factory->state(App\Models\InvoiceItem::class,'month',[ 'date_start'=>Carbon::now()->startOfMonth(), 'date_stop'=>Carbon::now()->endOfMonth(), + 'item_type'=>0, ]); $factory->state(App\Models\InvoiceItem::class,'month-mid',[ 'date_start'=>Carbon::now()->startOfMonth(), 'date_stop'=>Carbon::now()->endOfMonth()->addDays(Carbon::now()->daysInMonth/2+1), + 'item_type'=>0, ]); // Quarterly $factory->state(App\Models\InvoiceItem::class,'quarter',[ 'date_start'=>Carbon::now()->startOfQuarter(), 'date_stop'=>Carbon::now()->endOfQuarter(), + 'item_type'=>0, ]); $factory->state(App\Models\InvoiceItem::class,'quarter-mid',[ 'date_start'=>Carbon::now()->startOfQuarter(), 'date_stop'=>Carbon::now()->startOfQuarter()->addDays(45), + 'item_type'=>0, ]); // Half Yearly $factory->state(App\Models\InvoiceItem::class,'half',[ 'date_start'=>Carbon::now()->startOfHalf(), 'date_stop'=>Carbon::now()->endOfHalf(), + 'item_type'=>0, ]); $factory->state(App\Models\InvoiceItem::class,'half-mid',[ 'date_start'=>Carbon::now()->startOfHalf(), 'date_stop'=>Carbon::now()->startOfHalf()->addDays(90), + 'item_type'=>0, ]); // Yearly $factory->state(App\Models\InvoiceItem::class,'year',[ 'date_start'=>Carbon::now()->startOfYear(), 'date_stop'=>Carbon::now()->endOfYear(), + 'item_type'=>0, ]); $factory->state(App\Models\InvoiceItem::class,'year-mid',[ 'date_start'=>Carbon::now()->startOfYear(), 'date_stop'=>Carbon::now()->startOfYear()->addDays(181), + 'item_type'=>0, ]); // Two Yearly (price_recurr_strict ignored) $factory->state(App\Models\InvoiceItem::class,'2year',[ 'date_start'=>Carbon::now()->subyear(), 'date_stop'=>Carbon::now()->subday(), + 'item_type'=>0, ]); // Three Yearly (price_recurr_strict ignored) $factory->state(App\Models\InvoiceItem::class,'3year',[ 'date_start'=>Carbon::now()->subyear(2), '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, ]); \ No newline at end of file diff --git a/database/factories/ProductFactory.php b/database/factories/ProductFactory.php index 817b81f..5a6d984 100644 --- a/database/factories/ProductFactory.php +++ b/database/factories/ProductFactory.php @@ -5,15 +5,25 @@ use Faker\Generator as Faker; $factory->define(App\Models\Product::class, function (Faker $faker) { return [ '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',[ - 'active' => '1', + 'active' => 1, ]); $factory->state(App\Models\Product::class,'strict',[ - 'price_recurr_strict' => '1', + 'price_recurr_strict' => 1, ]); $factory->state(App\Models\Product::class,'notstrict',[ - 'price_recurr_strict' => '0', + 'price_recurr_strict' => 0, ]); \ No newline at end of file diff --git a/database/factories/ServiceFactory.php b/database/factories/ServiceFactory.php index a7002d6..6be4c7f 100644 --- a/database/factories/ServiceFactory.php +++ b/database/factories/ServiceFactory.php @@ -5,6 +5,7 @@ use Faker\Generator as Faker; $factory->define(App\Models\Service::class, function (Faker $faker) { return [ 'account_id'=>1, + 'active'=>1, ]; }); @@ -92,4 +93,17 @@ $factory->afterMakingState(App\Models\Service::class,'3year',function ($service, $invoice_items = factory(App\Models\InvoiceItem::class,1)->state('3year')->make(); $service->setRelation('invoice_items',$invoice_items); $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); }); \ No newline at end of file diff --git a/resources/theme/backend/adminlte/u/service/widgets/information.blade.php b/resources/theme/backend/adminlte/u/service/widgets/information.blade.php index db20cdb..e147ba4 100644 --- a/resources/theme/backend/adminlte/u/service/widgets/information.blade.php +++ b/resources/theme/backend/adminlte/u/service/widgets/information.blade.php @@ -41,7 +41,7 @@ @if ($o->autopay)Direct Debit @else Invoice @endif - @else + @elseif(! $o->wasCancelled()) Cancelled {!! $o->date_end ? $o->date_end->format('Y-m-d') : $o->paid_to->format('Y-m-d').'*' !!} diff --git a/tests/Feature/InvoiceTest.php b/tests/Feature/InvoiceTest.php new file mode 100644 index 0000000..2f9bab1 --- /dev/null +++ b/tests/Feature/InvoiceTest.php @@ -0,0 +1,33 @@ +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'); + } +} \ No newline at end of file diff --git a/tests/Feature/ServiceTest.php b/tests/Feature/ServiceTest.php index 3590cd2..12c68c6 100644 --- a/tests/Feature/ServiceTest.php +++ b/tests/Feature/ServiceTest.php @@ -12,7 +12,7 @@ use Tests\TestCase; class ServiceTest extends TestCase { /** - * A basic feature test example. + * Test billing calculations * * @return void */