diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c5f8cec..9b75b35 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,7 +1,7 @@ # This file is a template, and might need editing before it works on your project. # Official framework image. Look for the different tagged releases at: # https://hub.docker.com/r/library/php -image: registry.leenooks.net/leenooks/php:7.2-fpm-plus +image: registry.leenooks.net/leenooks/php:7.3-fpm-plus # Pick zero or more services to be used on all builds. # Only needed when using a docker container to run your tests in. diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 043cad6..0d4c550 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -3,6 +3,7 @@ namespace App\Exceptions; use Exception; +use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; class Handler extends ExceptionHandler @@ -46,6 +47,10 @@ class Handler extends ExceptionHandler */ public function render($request, Exception $exception) { + // We'll render a 404 for any authorisation exceptions to hide the fact that the resource exists + if ($exception instanceof AuthorizationException) + abort(404,'Not here...'); + return parent::render($request, $exception); } } diff --git a/app/Http/Controllers/User/AccountController.php b/app/Http/Controllers/User/AccountController.php new file mode 100644 index 0000000..b5aeafb --- /dev/null +++ b/app/Http/Controllers/User/AccountController.php @@ -0,0 +1,43 @@ +account = $o; + + // Get the account services + $s = $o->services(TRUE) + ->with(['invoice_items','charges']) + ->get() + ->filter(function($item) { + return ! $item->suspend_billing AND ! $item->external_billing; + }); + + // Get our invoice due date for this invoice + $io->due_date = $s->min(function($item) { return $item->invoice_next; }); + + // @todo The days in advance is an application parameter + $io->date_orig = $io->due_date->subDays(30); + + // Work out items to add to this invoice, plus any in the next additional days + $days = now()->diffInDays($io->due_date)+1+7; + foreach ($s as $so) + { + if ($so->isInvoiceDueSoon($days)) + foreach ($so->next_invoice_items() as $o) + $io->items->push($o); + } + + return View('u.invoice',['o'=>$io]); + } +} \ No newline at end of file diff --git a/app/Models/Account.php b/app/Models/Account.php index b3c5be0..6973f3c 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -60,9 +60,11 @@ class Account extends Model return $this->hasMany(Payment::class); } - public function services() + public function services($active=FALSE) { - return $this->hasMany(Service::class); + $query = $this->hasMany(Service::class); + + return $active ? $query->where('active','=',TRUE) : $query; } public function user() @@ -150,7 +152,7 @@ class Account extends Model public function getServicesCountHtmlAttribute() { - return sprintf('%s /%s',$this->services->where('active',TRUE)->count(),$this->services->count()); + return sprintf('%s /%s',$this->services()->noEagerLoads()->where('active',TRUE)->count(),$this->services()->noEagerLoads()->count()); } public function getSwitchUrlAttribute() diff --git a/app/Models/Charge.php b/app/Models/Charge.php index 2616087..91de1bd 100644 --- a/app/Models/Charge.php +++ b/app/Models/Charge.php @@ -7,6 +7,8 @@ use Illuminate\Database\Eloquent\Model; class Charge extends Model { protected $table = 'ab_charge'; + protected $dates = ['date_charge']; + public $dateFormat = 'U'; public function getNameAttribute() { diff --git a/app/Models/Country.php b/app/Models/Country.php index 1547bdf..d8f5b52 100644 --- a/app/Models/Country.php +++ b/app/Models/Country.php @@ -17,6 +17,11 @@ class Country extends Model return $this->belongsTo(Currency::class); } + public function taxes() + { + return $this->hasMany(Tax::class); + } + /** * The accounts in this country */ diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index c51f2fa..c198090 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -143,6 +143,8 @@ class Invoice extends Model { $return = collect(); + $this->items->load(['service']); + foreach ($this->items->filter(function ($item) use ($po) { return $item->product_id == $po->id; }) as $o) diff --git a/app/Models/InvoiceItem.php b/app/Models/InvoiceItem.php index c58de42..bb6436e 100644 --- a/app/Models/InvoiceItem.php +++ b/app/Models/InvoiceItem.php @@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Model; class InvoiceItem extends Model { protected $dates = ['date_start','date_stop']; + public $dateFormat = 'U'; protected $table = 'ab_invoice_item'; protected $with = ['taxes']; @@ -32,6 +33,8 @@ class InvoiceItem extends Model return $this->hasMany(InvoiceItemTax::class); } + /** ATTRIBUTES **/ + public function getItemTypeNameAttribute() { $types = [ @@ -69,22 +72,10 @@ class InvoiceItem extends Model } } - public function module_text() - { - switch ($this->module_id) - { - // Charges Module - case 30: return Charge::findOrFail($this->module_ref)->name; - - default: abort(500,'Unable to handle '.$this->module_id); - } - - } public function getSubTotalAttribute() { return $this->quantity * $this->price_base; } - public function getTaxAttribute() { if (! $this->_tax) @@ -102,4 +93,36 @@ class InvoiceItem extends Model { return $this->tax + $this->sub_total; } + + /** FUNCTIONS **/ + + /** + * Add taxes to this record + */ + public function addTaxes() + { + // Refuse to change an existing record + if ($this->exists) + throw new \Exception('Refusing to add Taxes to existing record'); + + foreach($this->service->account->country->taxes as $to) + { + $iit = new InvoiceItemTax; + $iit->tax_id = $to->id; + $iit->amount = round($this->quantity*$this->price_base*$to->rate,3); + $this->taxes->push($iit); + } + } + + public function module_text() + { + switch ($this->module_id) + { + // Charges Module + case 30: return Charge::findOrFail($this->module_ref)->name; + + default: abort(500,'Unable to handle '.$this->module_id); + } + + } } \ No newline at end of file diff --git a/app/Models/Policies/AccountPolicy.php b/app/Models/Policies/AccountPolicy.php new file mode 100644 index 0000000..1f26c48 --- /dev/null +++ b/app/Models/Policies/AccountPolicy.php @@ -0,0 +1,91 @@ +accounts->pluck('id')->search($o->id)) + + // The user is the wholesaler + OR $user->isWholesaler() + + // The user is the reseller + OR $user->all_accounts()->pluck('id')->search($o->id); + } + + /** + * Determine whether the user can create services. + * + * @param \App\User $user + * @return mixed + */ + public function create(User $user) + { + return TRUE; + } + + /** + * Determine whether the user can update the service. + * + * @param \App\User $user + * @param Account $o + * @return mixed + */ + public function update(User $user, Account $o) + { + return $user->isWholesaler(); + } + + /** + * Determine whether the user can delete the service. + * + * @param \App\User $user + * @param Account $o + * @return mixed + */ + public function delete(User $user, Account $o) + { + return $user->isWholesaler(); + } + + /** + * Determine whether the user can restore the service. + * + * @param \App\User $user + * @param Account $o + * @return mixed + */ + public function restore(User $user, Account $o) + { + return $user->isWholesaler(); + } + + /** + * Determine whether the user can permanently delete the service. + * + * @param \App\User $user + * @param Account $o + * @return mixed + */ + public function forceDelete(User $user, Account $o) + { + return $user->isWholesaler(); + } +} \ No newline at end of file diff --git a/app/Models/Policies/InvoicePolicy.php b/app/Models/Policies/InvoicePolicy.php new file mode 100644 index 0000000..8915b62 --- /dev/null +++ b/app/Models/Policies/InvoicePolicy.php @@ -0,0 +1,91 @@ +invoices->pluck('id')->search($o->id)) + + // The user is the wholesaler + OR $user->isWholesaler() + + // The user is the reseller + OR $user->all_accounts()->pluck('id')->search($o->account_id); + } + + /** + * Determine whether the user can create services. + * + * @param \App\User $user + * @return mixed + */ + public function create(User $user) + { + return TRUE; + } + + /** + * Determine whether the user can update the service. + * + * @param \App\User $user + * @param Invoice $o + * @return mixed + */ + public function update(User $user, Invoice $o) + { + return $user->isWholesaler(); + } + + /** + * Determine whether the user can delete the service. + * + * @param \App\User $user + * @param Invoice $o + * @return mixed + */ + public function delete(User $user, Invoice $o) + { + return $user->isWholesaler(); + } + + /** + * Determine whether the user can restore the service. + * + * @param \App\User $user + * @param Invoice $o + * @return mixed + */ + public function restore(User $user, Invoice $o) + { + return $user->isWholesaler(); + } + + /** + * Determine whether the user can permanently delete the service. + * + * @param \App\User $user + * @param Invoice $o + * @return mixed + */ + public function forceDelete(User $user, Invoice $o) + { + return $user->isWholesaler(); + } +} \ No newline at end of file diff --git a/app/Models/Policies/ServicePolicy.php b/app/Models/Policies/ServicePolicy.php new file mode 100644 index 0000000..5ea586a --- /dev/null +++ b/app/Models/Policies/ServicePolicy.php @@ -0,0 +1,91 @@ +services->pluck('id')->search($o->id)) + + // The user is the wholesaler + OR $user->isWholesaler() + + // The user is the reseller + OR $user->all_accounts()->pluck('id')->search($o->account_id); + } + + /** + * Determine whether the user can create services. + * + * @param \App\User $user + * @return mixed + */ + public function create(User $user) + { + return TRUE; + } + + /** + * Determine whether the user can update the service. + * + * @param \App\User $user + * @param Service $o + * @return mixed + */ + public function update(User $user, Service $o) + { + return $user->isWholesaler(); + } + + /** + * Determine whether the user can delete the service. + * + * @param \App\User $user + * @param Service $o + * @return mixed + */ + public function delete(User $user, Service $o) + { + return $user->isWholesaler(); + } + + /** + * Determine whether the user can restore the service. + * + * @param \App\User $user + * @param Service $o + * @return mixed + */ + public function restore(User $user, Service $o) + { + return $user->isWholesaler(); + } + + /** + * Determine whether the user can permanently delete the service. + * + * @param \App\User $user + * @param Service $o + * @return mixed + */ + public function forceDelete(User $user, Service $o) + { + return $user->isWholesaler(); + } +} \ No newline at end of file diff --git a/app/Models/Product.php b/app/Models/Product.php index 8057235..9fc109d 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -79,7 +79,7 @@ class Product extends Model private function getDefaultLanguage() { - return config('SITE_SETUP')->language(); + return config('SITE_SETUP')->language; } public function getDescriptionAttribute() @@ -179,9 +179,21 @@ class Product extends Model } } + /** + * Get the price for this product based on the period being requested. + * + * If the price period doesnt exist, we'll take the default period (0) which should. + * + * @param int $period + * @return mixed + */ public function price(int $period) { - return Arr::get($this->price_array,sprintf('%s.1.price_base',$period)); + return Arr::get( + $this->price_array, + sprintf('%s.1.price_base',$period), + Arr::get($this->price_array,sprintf('%s.0.price_base',$period)) + ); } public function PricePeriods() diff --git a/app/Models/Service.php b/app/Models/Service.php index 540d91d..1063dbf 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -30,6 +30,7 @@ class Service extends Model protected $appends = [ 'account_name', 'admin_service_id_url', + 'billing_price', 'name_short', 'next_invoice', 'product_category', @@ -43,6 +44,7 @@ class Service extends Model 'account_name', 'admin_service_id_url', 'active', + 'billing_price', 'data_orig', 'id', 'name_short', @@ -207,6 +209,16 @@ class Service extends Model }); } + /** + * Enable to perform queries without eager loading + * + * @param $query + * @return mixed + */ + public function scopeNoEagerLoads($query){ + return $query->setEagerLoads([]); + } + /** ATTRIBUTES **/ /** @@ -229,7 +241,11 @@ class Service extends Model public function getBillingPriceAttribute(): float { - return $this->addTax($this->price ?: $this->product->price($this->recur_schedule)); + // @todo Temporary for services that dont have recur_schedule set. + if (is_null($this->recur_schedule) OR is_null($this->product->price($this->recur_schedule))) + $this->price=0; + + return $this->addTax(is_null($this->price) ? $this->product->price($this->recur_schedule) : $this->price); } /** @@ -481,7 +497,7 @@ class Service extends Model * @param float $value * @return float */ - public function addTax(float $value): float + private function addTax(float $value): float { return round($value*1.1,2); } @@ -506,23 +522,51 @@ class Service extends Model return $this->active OR ($this->order_status AND ! in_array($this->order_status,$this->inactive_status)); } - public function next_invoice_items() + /** + * Should this service be invoiced soon + * + * @todo get the number of days from account setup + * @return bool + */ + public function isInvoiceDueSoon($days=30): bool + { + return (! $this->external_billing) AND (! $this->suspend_billing) AND $this->getInvoiceNextAttribute()->lessThan(now()->addDays($days)); + } + + public function next_invoice_items(): \Illuminate\Support\Collection { $result = collect(); - $result->push([ - 'item'=>0, - 'desc'=>sprintf('Product/Service [%s->%s]', - $this->invoice_next->format('Y-m-d'), - $this->invoice_next_end->format('Y-m-d')), - 'amt'=>$this->getBillingPriceAttribute()]); + $o = new InvoiceItem; + $o->active = TRUE; + $o->service_id = $this->id; + $o->product_id = $this->product_id; + $o->quantity = 1; + $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; - foreach ($this->charges->filter(function($item) { return ! $item->processed; }) as $o) + $o->addTaxes(); + $result->push($o); + + foreach ($this->charges->filter(function($item) { return ! $item->processed; }) as $oo) { - $result->push([ - 'item'=>5, // @tod Charges - 'desc'=>sprintf('%d@%3.2f - %s',$o->quantity,$this->addTax($o->amount),$o->name), - 'amt'=>$this->addTax($o->amount*$o->quantity)]); + $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->date_start = $oo->date_charge; + $o->date_stop = $oo->date_charge; + $o->module_id = 30; // @todo This shouldnt be hard coded + $o->module_ref = $oo->id; + + $o->addTaxes(); + $result->push($o); } return $result; diff --git a/app/Models/Site.php b/app/Models/Site.php index 20cdf00..9e4c0de 100644 --- a/app/Models/Site.php +++ b/app/Models/Site.php @@ -10,7 +10,7 @@ class Site extends Model protected $table = 'ab_setup'; public $timestamps = FALSE; - protected $with = ['details']; + protected $with = ['details','language']; protected $casts = [ 'address'=>'array', @@ -23,9 +23,6 @@ class Site extends Model public function language() { - if (! $this->id) - return Language::find(1); - return $this->belongsTo(Language::class); } @@ -151,6 +148,7 @@ class Site extends Model ], ], 'clients_intro'=>'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore', + 'language_id'=>1, 'page_tabs'=>[ [ 'title'=>'Title 1', diff --git a/app/Models/Tax.php b/app/Models/Tax.php new file mode 100644 index 0000000..7445581 --- /dev/null +++ b/app/Models/Tax.php @@ -0,0 +1,12 @@ + @section('page-scripts') - @css('https://cdn.datatables.net/1.10.19/css/jquery.dataTables.min.css','jq-dt-css','jquery'); - @js('https://cdn.datatables.net/1.10.19/js/jquery.dataTables.min.js','jq-dt-js','jquery'); - @css('https://cdn.datatables.net/responsive/2.2.1/css/responsive.dataTables.min.css','dt-responsive-css','jq-dt-css') - @js('https://cdn.datatables.net/responsive/2.2.1/js/dataTables.responsive.min.js','dt-responsive-js','jq-dt-js') - @css('https://cdn.datatables.net/rowgroup/1.0.2/css/rowGroup.dataTables.min.css','dt-rowgroup-css','jq-dt-css') - @js('https://cdn.datatables.net/rowgroup/1.0.2/js/dataTables.rowGroup.min.js','dt-rowgroup-js','jq-dt-js') + @css('//cdn.datatables.net/1.10.19/css/jquery.dataTables.min.css','jq-dt-css','jquery'); + @js('//cdn.datatables.net/1.10.19/js/jquery.dataTables.min.js','jq-dt-js','jquery'); + @css('//cdn.datatables.net/responsive/2.2.1/css/responsive.dataTables.min.css','dt-responsive-css','jq-dt-css') + @js('//cdn.datatables.net/responsive/2.2.1/js/dataTables.responsive.min.js','dt-responsive-js','jq-dt-js') @css('/plugin/dataTables/dataTables.bootstrap4.css','dt-bootstrap4-css','jq-dt-css') @js('/plugin/dataTables/dataTables.bootstrap4.js','dt-bootstrap4-js','jq-dt-js') diff --git a/resources/theme/backend/adminlte/common/invoice/widget/list.blade.php b/resources/theme/backend/adminlte/common/invoice/widget/list.blade.php index ebfa1e3..fad6f39 100644 --- a/resources/theme/backend/adminlte/common/invoice/widget/list.blade.php +++ b/resources/theme/backend/adminlte/common/invoice/widget/list.blade.php @@ -24,10 +24,10 @@ @section('page-scripts') - @css('https://cdn.datatables.net/1.10.19/css/jquery.dataTables.min.css','jq-dt-css','jquery'); - @js('https://cdn.datatables.net/1.10.19/js/jquery.dataTables.min.js','jq-dt-js','jquery'); - @css('https://cdn.datatables.net/responsive/2.2.1/css/responsive.dataTables.min.css','jq-dt-r-css','jq-dt-css') - @js('https://cdn.datatables.net/responsive/2.2.1/js/dataTables.responsive.min.js','jq-dt-r-js','jq-dt-js') + @css('//cdn.datatables.net/1.10.19/css/jquery.dataTables.min.css','jq-dt-css','jquery'); + @js('//cdn.datatables.net/1.10.19/js/jquery.dataTables.min.js','jq-dt-js','jquery'); + @css('//cdn.datatables.net/responsive/2.2.1/css/responsive.dataTables.min.css','jq-dt-r-css','jq-dt-css') + @js('//cdn.datatables.net/responsive/2.2.1/js/dataTables.responsive.min.js','jq-dt-r-js','jq-dt-js') @css('/plugin/dataTables/dataTables.bootstrap4.css','dt-bootstrap4-css','jq-dt-css') @js('/plugin/dataTables/dataTables.bootstrap4.js','dt-bootstrap4-js','jq-dt-js') diff --git a/resources/theme/backend/adminlte/common/payment/widget/history.blade.php b/resources/theme/backend/adminlte/common/payment/widget/history.blade.php index d7c7930..852e6a8 100644 --- a/resources/theme/backend/adminlte/common/payment/widget/history.blade.php +++ b/resources/theme/backend/adminlte/common/payment/widget/history.blade.php @@ -26,12 +26,10 @@ @section('page-scripts') - @css('https://cdn.datatables.net/1.10.19/css/jquery.dataTables.min.css','jq-dt-css','jquery'); - @js('https://cdn.datatables.net/1.10.19/js/jquery.dataTables.min.js','jq-dt-js','jquery'); - @css('https://cdn.datatables.net/responsive/2.2.1/css/responsive.dataTables.min.css','dt-responsive-css','jq-dt-css') - @js('https://cdn.datatables.net/responsive/2.2.1/js/dataTables.responsive.min.js','dt-responsive-js','jq-dt-js') - @css('https://cdn.datatables.net/rowgroup/1.0.2/css/rowGroup.dataTables.min.css','dt-rowgroup-css','jq-dt-css') - @js('https://cdn.datatables.net/rowgroup/1.0.2/js/dataTables.rowGroup.min.js','dt-rowgroup-js','jq-dt-js') + @css('//cdn.datatables.net/1.10.19/css/jquery.dataTables.min.css','jq-dt-css','jquery'); + @js('//cdn.datatables.net/1.10.19/js/jquery.dataTables.min.js','jq-dt-js','jquery'); + @css('//cdn.datatables.net/responsive/2.2.1/css/responsive.dataTables.min.css','dt-responsive-css','jq-dt-css') + @js('//cdn.datatables.net/responsive/2.2.1/js/dataTables.responsive.min.js','dt-responsive-js','jq-dt-js') @css('/plugin/dataTables/dataTables.bootstrap4.css','dt-bootstrap4-css','jq-dt-css') @js('/plugin/dataTables/dataTables.bootstrap4.js','dt-bootstrap4-js','jq-dt-js') diff --git a/resources/theme/backend/adminlte/common/service/widget/active.blade.php b/resources/theme/backend/adminlte/common/service/widget/active.blade.php index 3636d76..afcb6df 100644 --- a/resources/theme/backend/adminlte/common/service/widget/active.blade.php +++ b/resources/theme/backend/adminlte/common/service/widget/active.blade.php @@ -14,6 +14,7 @@
{{ $o->name }} | {{ number_format($o->next_invoice_items()->sum('amt'),2) }} | +{{ $o->name }} | ${{ number_format($o->next_invoice_items()->sum('total'),2) }} | ||||
---|---|---|---|---|---|---|---|
{{ $oo['desc'] }} | {{ number_format($oo['amt'],2) }} | +{{ $io->item_type_name }} | ${{ number_format($io->total,2) }} |