diff --git a/app/Models/Account.php b/app/Models/Account.php index 903c1ed..7dff235 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -2,14 +2,13 @@ namespace App\Models; -use Awobaz\Compoships\Compoships; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Collection; +use Illuminate\Support\Facades\DB; use Leenooks\Traits\ScopeActive; -use App\Models\Scopes\SiteScope; use App\Interfaces\IDs; -use App\Traits\SiteID; /** * Class Account @@ -20,12 +19,10 @@ use App\Traits\SiteID; * + sid : System ID for account * + name : Account Name * + taxes : Taxes Applicable to this account - * - * @package App\Models */ class Account extends Model implements IDs { - use Compoships,HasFactory,ScopeActive,SiteID; + use HasFactory,ScopeActive; /* INTERFACES */ @@ -41,13 +38,18 @@ class Account extends Model implements IDs /* RELATIONS */ + /** + * Charges assigned to this account + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ public function charges() { return $this->hasMany(Charge::class); } /** - * Return the country the user belongs to + * Country this account belongs to */ public function country() { @@ -65,28 +67,47 @@ class Account extends Model implements IDs } /** - * @return mixed + * Invoices created for this account + * * @todo This needs to be optimised, to only return outstanding invoices and invoices for a specific age (eg: 2 years worth) */ public function invoices() { return $this->hasMany(Invoice::class) - ->active() ->with(['items.taxes','paymentitems.payment']); } - public function language() + /** + * Relation to only return active invoices + * + * @todo Only return active invoice_items + */ + public function invoices_active() { - return $this->belongsTo(Language::class); + return $this->invoices() + ->active(); } + /** + * Payments received and assigned to this account + */ public function payments() { return $this->hasMany(Payment::class) - ->active() ->with(['items']); } + /** + * Relation to only return active payments + * + * @todo Only return active payment_items + */ + public function payments_active() + { + return $this->payments() + ->active(); + } + public function providers() { return $this->belongsToMany(ProviderOauth::class,'account__provider') @@ -94,25 +115,33 @@ class Account extends Model implements IDs ->withPivot('ref','synctoken','created_at','updated_at'); } - public function services($active=FALSE) + /** + * Services assigned to this account + */ + public function services() { - $query = $this->hasMany(Service::class,['account_id','site_id'],['id','site_id']) - ->withoutGlobalScope(SiteScope::class) + return $this->hasMany(Service::class) ->with(['product.translate','invoice_items']); - - return $active ? $query->active() : $query; } - public function site() + /** + * Relation to only return active services + */ + public function services_active() { - return $this->belongsTo(Site::class); + return $this->services() + ->active(); } public function taxes() { - return $this->hasMany(Tax::class,'country_id','country_id'); + return $this->hasMany(Tax::class,'country_id','country_id') + ->select(['id','zone','rate','country_id']); } + /** + * User that owns this account + */ public function user() { return $this->belongsTo(User::class); @@ -149,30 +178,27 @@ class Account extends Model implements IDs * Get the address for the account * * @return array + * @todo Change this to return a collection */ public function getAddressAttribute(): array { - return [ - $this->address1, - $this->address2, - sprintf('%s %s %s',$this->city.(($this->state OR $this->zip) ? ',' : ''),$this->state,$this->zip) - ]; - } - - /** - * Account breadcrumb to render on pages - * - * @return array - */ - public function getBreadcrumbAttribute(): array - { - return [$this->name => url('u/home',$this->user_id)]; + return collect([ + 'address1' => $this->address1, + 'address2' => $this->address2, + 'location' => sprintf('%s %s %s', + $this->city.(($this->state || $this->zip) ? ',' : ''), + $this->state, + $this->zip) + ]) + ->filter() + ->values() + ->toArray(); } /** * Return the account name * - * @return mixed|string + * @return string */ public function getNameAttribute(): string { @@ -184,7 +210,7 @@ class Account extends Model implements IDs * * @return string */ - public function getTypeAttribute() + public function getTypeAttribute(): string { return $this->company ? 'Business' : 'Private'; } @@ -195,6 +221,7 @@ class Account extends Model implements IDs * Get the due invoices on an account * * @return mixed + * @deprecated use invoiceSummary->filter(_balance > 0) */ public function dueInvoices() { @@ -203,6 +230,66 @@ class Account extends Model implements IDs }); } + /** + * List of invoices (summary) for this account + * + * @param Collection|NULL $invoices + * @return Collection + */ + public function invoiceSummary(Collection $invoices=NULL): Collection + { + return (new Invoice) + ->select([ + 'invoice_id as id', + DB::raw('SUM(item) AS _item'), + DB::raw('SUM(tax) AS _tax'), + DB::raw('SUM(payments) AS _payment'), + DB::raw('SUM(discount) AS _discount'), + DB::raw('SUM(item_total) AS _item_total'), + DB::raw('SUM(payment_fees) AS _payment_fee'), + DB::raw('ROUND(CAST(SUM(item_total)-SUM(COALESCE(discount,0))+COALESCE(invoices.discount_amt,0) AS NUMERIC),2) AS _total'), + DB::raw('ROUND(CAST(SUM(item_total)-SUM(COALESCE(discount,0))+COALESCE(invoices.discount_amt,0)-SUM(payments) AS NUMERIC),2) AS _balance'), + 'due_at', + ]) + ->from( + (new Payment) + ->select([ + 'invoice_id', + DB::raw('0 as item'), + DB::raw('0 as tax'), + DB::raw('0 as discount'), + DB::raw('0 as item_total'), + DB::raw('SUM(amount) AS payments'), + DB::raw('SUM(fees_amt) AS payment_fees'), + ]) + ->join('payment_items',['payment_items.payment_id'=>'payments.id']) + ->where('payments.active',TRUE) + ->where('payment_items.active',TRUE) + ->groupBy(['payment_items.invoice_id']) + ->union( + (new InvoiceItem) + ->select([ + 'invoice_id', + DB::raw('ROUND(CAST(SUM(quantity*price_base) AS NUMERIC),2) AS item'), + DB::raw('ROUND(CAST(SUM(amount) AS NUMERIC),2) AS tax'), + DB::raw('SUM(COALESCE(invoice_items.discount_amt,0)) AS discount'), + DB::raw('ROUND(CAST(SUM(ROUND(CAST(quantity*price_base AS NUMERIC),2))+SUM(ROUND(CAST(amount AS NUMERIC),2))-SUM(COALESCE(invoice_items.discount_amt,0)) AS NUMERIC),2) AS item_total'), + DB::raw('0 as payments'), + DB::raw('0 as payment_fees'), + ]) + ->leftjoin('invoice_item_taxes',['invoice_item_taxes.invoice_item_id'=>'invoice_items.id']) + ->rightjoin('invoices',['invoices.id'=>'invoice_items.invoice_id']) + ->where('invoice_items.active',TRUE) + ->where('invoices.active',TRUE) + ->groupBy(['invoice_items.invoice_id']), + ),'p') + ->join('invoices',['invoices.id'=>'invoice_id']) + ->where('account_id',$this->id) + ->groupBy(['p.invoice_id']) + ->groupBy(['due_at','discount_amt']) + ->get(); + } + /** * Return the taxed value of a value * diff --git a/app/Models/Charge.php b/app/Models/Charge.php index 85528b0..e210c67 100644 --- a/app/Models/Charge.php +++ b/app/Models/Charge.php @@ -57,16 +57,18 @@ class Charge extends Model /* SCOPES */ + /** @deprecated use pending */ public function scopeUnprocessed($query) { + return $this->scopePending(); + } + + public function scopePending($query) { return $query - ->where('active',TRUE) + ->active() ->whereNotNull('charge_at') ->whereNotNull('type') - ->where(function($q) { - return $q->where('processed',FALSE) - ->orWhereNull('processed'); - }); + ->where(fn($query)=>$query->where('processed',FALSE)->orWhereNull('processed')); } /* ATTRIBUTES */ diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index ea8b90a..0a7a45a 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -2,7 +2,6 @@ namespace App\Models; -use Awobaz\Compoships\Compoships; use Carbon\Carbon; use Clarkeash\Doorman\Facades\Doorman; use Clarkeash\Doorman\Models\Invite; @@ -11,7 +10,7 @@ use Illuminate\Support\Arr; use Leenooks\Traits\ScopeActive; use App\Interfaces\IDs; -use App\Traits\{PushNew,SiteID}; +use App\Traits\PushNew; /** * Class Invoice @@ -34,14 +33,11 @@ use App\Traits\{PushNew,SiteID}; */ class Invoice extends Model implements IDs { - use Compoships,PushNew,ScopeActive,SiteID; + use PushNew,ScopeActive; protected $casts = [ 'reminders'=>'json', - ]; - - protected $dates = [ - 'due_at', + 'due_at'=>'datetime:y-m-d', ]; public const BILL_WEEKLY = 0; @@ -105,11 +101,6 @@ class Invoice extends Model implements IDs ]; */ - // Caching variables - private int $_paid = 0; - private int $_total = 0; - private int $_total_tax = 0; - /* STATIC METHODS */ /** diff --git a/app/Models/Service.php b/app/Models/Service.php index 71508cb..af047a1 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -2,7 +2,6 @@ namespace App\Models; -use Awobaz\Compoships\Compoships; use Carbon\Carbon; use Exception; use Illuminate\Database\Eloquent\Casts\AsCollection; @@ -19,10 +18,8 @@ use Symfony\Component\HttpKernel\Exception\HttpException; use Leenooks\Carbon as LeenooksCarbon; use App\Models\Product\Type; -use App\Models\Scopes\SiteScope; use App\Interfaces\IDs; use App\Traits\ScopeServiceUserAuthorised; -use App\Traits\SiteID; /** * Class Service @@ -60,24 +57,23 @@ use App\Traits\SiteID; */ class Service extends Model implements IDs { - use HasFactory,ScopeServiceUserAuthorised,SiteID,Compoships; + use HasFactory,ScopeServiceUserAuthorised; protected $casts = [ - 'order_info'=>AsCollection::class, - ]; - - protected $dates = [ - 'invoice_last_at', - 'invoice_next_at', - 'start_at', - 'stop_at', + 'order_info' => AsCollection::class, + 'invoice_last_at' => 'datetime:Y-m-d', // @todo Can these be removed, since we can work out invoice dynamically now + 'invoice_next_at' => 'datetime:Y-m-d', // @todo Can these be removed, since we can work out invoice dynamically now + 'stop_at' => 'datetime:Y-m-d', + 'start_at' => 'datetime:Y-m-d', ]; + /** @deprecated */ protected $appends = [ 'category_name', 'name_short', ]; + /** @deprecated */ protected $visible = [ // 'account_name', // 'admin_service_id_url', @@ -95,8 +91,6 @@ class Service extends Model implements IDs ]; protected $with = [ - //'invoice_items', - //'product.type.supplied', 'type', ]; @@ -332,8 +326,7 @@ class Service extends Model implements IDs */ public function account() { - return $this->belongsTo(Account::class,['account_id','site_id'],['id','site_id']) - ->withoutGlobalScope(SiteScope::class); + return $this->belongsTo(Account::class); } /** @@ -353,12 +346,31 @@ class Service extends Model implements IDs */ public function charges() { - return $this->hasMany(Charge::class,['service_id','site_id'],['id','site_id']) - ->withoutGlobalScope(SiteScope::class) - ->where('active','=',TRUE) + return $this->hasMany(Charge::class) ->orderBy('created_at'); } + /** + * Return only the active charges + */ + public function charges_active() + { + return $this->charges() + ->active(); + } + + /** + * Return only the charges not yet processed + */ + public function charges_pending() + { + return $this->charges() + ->pending(); + } + + /** + * Product changes for this service + */ public function changes() { return $this->belongsToMany(Product::class,'service__change','service_id','product_id','id','id') @@ -367,53 +379,80 @@ class Service extends Model implements IDs ->withTimestamps(); } - // @todo changed to invoiced_items + /** + * @deprecated use invoiced_items + */ public function invoice_items($active=TRUE) { - $query = $this->hasMany(InvoiceItem::class,['service_id','site_id'],['id','site_id']) - ->withoutGlobalScope(SiteScope::class) - ->where('item_type','=',0) - ->orderBy('start_at'); - - // @todo Change to $query->active(); - if ($active) - $query->where('active','=',TRUE); - - return $query; + return $this->invoiced_items_active(); } /** - * Invoices for this service + * Invoices that this service is itemised on */ - public function invoices($active=TRUE) + public function invoiced_items() { - $query = $this->hasManyThrough(Invoice::class,InvoiceItem::class,NULL,'id',NULL,'invoice_id') - ->when($this->site_id,function($q) { - return $q->where('invoices.site_id', $this->site_id) - ->withoutGlobalScope(SiteScope::class); - }) - ->distinct('id') - ->where('invoices.site_id','=',$this->site_id) - ->where('invoice_items.site_id','=',$this->site_id) - ->orderBy('created_at') - ->orderBy('due_at'); - - if ($active) - $query->where('invoice_items.active','=',TRUE) - ->where('invoices.active','=',TRUE); - - return $query; + return $this->hasMany(InvoiceItem::class) + ->with(['taxes']); } /** - * Account that ordered the service + * Invoices that this service is itemised on that is active + */ + public function invoiced_items_active() + { + return $this->invoiced_items() + ->where('active',TRUE); + } + + /** + * Return the extra charged items for this service (ie: item_type != 0) + */ + public function invoiced_extra_items() + { + return $this->hasMany(InvoiceItem::class) + ->where('item_type','<>',0) + ->orderBy('service_id') + ->orderBy('start_at'); + } + + /** + * Return active extra items charged + */ + public function invoiced_extra_items_active() + { + return $this->invoiced_extra_items() + ->where('active',TRUE); + } + + /** + * Return the service charged items for this service (ie: item_type == 0) + */ + public function invoiced_service_items() + { + return $this->hasMany(InvoiceItem::class) + ->where('item_type','=',0) + ->orderBy('service_id') + ->orderBy('start_at'); + } + + /** + * Return active service items charged + */ + public function invoiced_service_items_active() + { + return $this->invoiced_service_items() + ->where('active',TRUE); + } + + /** + * User that ordered the service * * @return BelongsTo */ public function orderedby() { - return $this->belongsTo(Account::class,['ordered_by','site_id'],['id','site_id']) - ->withoutGlobalScope(SiteScope::class); + return $this->belongsTo(User::class); } /** @@ -423,8 +462,7 @@ class Service extends Model implements IDs */ public function product() { - return $this->belongsTo(Product::class,['product_id','site_id'],['id','site_id']) - ->withoutGlobalScope(SiteScope::class); + return $this->belongsTo(Product::class); } /** @@ -444,10 +482,11 @@ class Service extends Model implements IDs */ public function scopeActive($query) { - return $query->where(function () use ($query) { - $query->where($this->getTable().'.active',TRUE) - ->orWhereNotIn('order_status',self::INACTIVE_STATUS); - }); + return $query->where( + fn($query)=> + $query->where($this->getTable().'.active',TRUE) + ->orWhereNotIn('order_status',self::INACTIVE_STATUS) + ); } /** @@ -458,20 +497,11 @@ class Service extends Model implements IDs */ public function scopeInActive($query) { - return $query->where(function () use ($query) { - $query->where($this->getTable().'.active',FALSE) - ->orWhereIn('order_status',self::INACTIVE_STATUS); - }); - } - - /** - * Enable to perform queries without eager loading - * - * @param $query - * @return mixed - */ - public function scopeNoEagerLoads($query){ - return $query->setEagerLoads([]); + return $query->where( + fn($query)=> + $query->where($this->getTable().'.active',FALSE) + ->orWhereIn('order_status',self::INACTIVE_STATUS) + ); } /** @@ -723,7 +753,7 @@ class Service extends Model implements IDs if (! $this->product->price_recur_strict) return 1; - $n = $this->invoice_next->diff($this->invoice_next_end)->days+1; + $n = round($this->invoice_next->diffInDays($this->invoice_next_end),0); switch ($this->recur_schedule) { case Invoice::BILL_WEEKLY: @@ -735,7 +765,7 @@ class Service extends Model implements IDs break; case Invoice::BILL_QUARTERLY: - $d = $this->invoice_next->addQuarter()->startOfQuarter()->diff($this->invoice_next_end->startOfQuarter())->days; + $d = round($this->invoice_next_end->startOfQuarter()->diffInDays($this->invoice_next->addQuarter()->startOfQuarter()),1); break; case Invoice::BILL_SEMI_YEARLY: @@ -845,19 +875,15 @@ class Service extends Model implements IDs */ public function getPaidToAttribute(): ?Carbon { - if (! $this->invoices->count()) - return NULL; + // Last paid invoice + $lastpaid = $this + ->invoices() + ->filter(fn($item)=>$item->_balance <= 0) + ->last(); - foreach ($this->invoices->reverse() as $o) - if ($o->due == 0) - break; - - return $o->items - ->filter(function($item) { - return $item->item_type === 0; - }) - ->last() - ->stop_at; + return $lastpaid + ? $this->invoiced_service_items_active->where('invoice_id',$lastpaid->id)->where('type',0)->max('stop_at') + : NULL; } /** @@ -1148,6 +1174,15 @@ class Service extends Model implements IDs return $this->product->hasUsage(); } + /** + * Return this service invoices + */ + public function invoices() + { + return $this->account + ->invoiceSummary($this->invoiced_service_items_active->pluck('invoice_id')); + } + /** * Determine if a service is active. It is active, if active=1, or the order_status is not in self::INACTIVE_STATUS[] * diff --git a/app/Models/Service/Broadband.php b/app/Models/Service/Broadband.php index b49c7d4..936016a 100644 --- a/app/Models/Service/Broadband.php +++ b/app/Models/Service/Broadband.php @@ -20,10 +20,6 @@ class Broadband extends Type implements ServiceUsage { private const LOGKEY = 'MSB'; - protected $casts = [ - 'connect_at'=>'datetime:Y-m-d', - 'expire_at'=>'datetime:Y-m-d', - ]; protected $table = 'service_broadband'; /* INTERFACES */ diff --git a/app/Models/Service/Domain.php b/app/Models/Service/Domain.php index 83c57a8..50ee2c1 100644 --- a/app/Models/Service/Domain.php +++ b/app/Models/Service/Domain.php @@ -14,7 +14,9 @@ class Domain extends Type use ServiceDomains; protected $table = 'service_domain'; - protected $with = ['tld']; + protected $with = [ + 'tld', + ]; /* OVERRIDES */ diff --git a/app/Models/Service/Email.php b/app/Models/Service/Email.php index cc88325..80a0a35 100644 --- a/app/Models/Service/Email.php +++ b/app/Models/Service/Email.php @@ -13,5 +13,7 @@ class Email extends Type use ServiceDomains; protected $table = 'service_email'; - protected $with = ['tld']; + protected $with = [ + 'tld', + ]; } \ No newline at end of file diff --git a/app/Models/Service/Host.php b/app/Models/Service/Host.php index 35bec09..acf1efb 100644 --- a/app/Models/Service/Host.php +++ b/app/Models/Service/Host.php @@ -14,7 +14,9 @@ class Host extends Type use ServiceDomains; protected $table = 'service_host'; - protected $with = ['tld']; + protected $with = [ + 'tld', + ]; /* RELATIONS */ diff --git a/app/Models/Service/Phone.php b/app/Models/Service/Phone.php index dd58b1b..bf75731 100644 --- a/app/Models/Service/Phone.php +++ b/app/Models/Service/Phone.php @@ -8,10 +8,6 @@ namespace App\Models\Service; */ class Phone extends Type { - protected $dates = [ - 'connect_at', - 'expire_at', - ]; protected $table = 'service_phone'; /* INTERFACES */ diff --git a/app/Models/Service/Type.php b/app/Models/Service/Type.php index 5b8c847..599fa78 100644 --- a/app/Models/Service/Type.php +++ b/app/Models/Service/Type.php @@ -14,9 +14,11 @@ abstract class Type extends Model implements ServiceItem { use ScopeServiceActive,ScopeServiceUserAuthorised; - protected $dates = [ - 'expire_at', + protected $casts = [ + 'connect_at' => 'datetime:Y-m-d', + 'expire_at' => 'datetime:Y-m-d', ]; + public $timestamps = FALSE; /* RELATIONS */ diff --git a/composer.json b/composer.json index 210fbfc..a62f688 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,7 @@ "ext-curl": "*", "ext-pdo": "*", "barryvdh/laravel-snappy": "^1.0", + "clarkeash/doorman": "^9.0", "eduardokum/laravel-mail-auto-embed": "^2.0", "laravel/dreamscape": "^0.1.0", "laravel/framework": "^11.0", diff --git a/composer.lock b/composer.lock index df1cada..f662133 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "65656094f15929a06f03ee8719037bdd", + "content-hash": "b84c28d616dc200d6583006761ae9e0d", "packages": [ { "name": "barryvdh/laravel-snappy", @@ -213,6 +213,62 @@ ], "time": "2024-02-09T16:56:22+00:00" }, + { + "name": "clarkeash/doorman", + "version": "v9.0.0", + "source": { + "type": "git", + "url": "https://github.com/clarkeash/doorman.git", + "reference": "e94b6be6a0996b0fd457a4165c4a45016969ba5e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clarkeash/doorman/zipball/e94b6be6a0996b0fd457a4165c4a45016969ba5e", + "reference": "e94b6be6a0996b0fd457a4165c4a45016969ba5e", + "shasum": "" + }, + "require": { + "laravel/framework": "^11.0", + "php": "^8.2", + "ramsey/uuid": "^4.0" + }, + "require-dev": { + "mockery/mockery": "^1.4", + "orchestra/testbench": "^9.0", + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Clarkeash\\Doorman\\Providers\\DoormanServiceProvider" + ], + "aliases": { + "Doorman": "Clarkeash\\Doorman\\Facades\\Doorman" + } + } + }, + "autoload": { + "psr-4": { + "Clarkeash\\Doorman\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ashley Clarke", + "email": "me@ashleyclarke.me" + } + ], + "support": { + "issues": "https://github.com/clarkeash/doorman/issues", + "source": "https://github.com/clarkeash/doorman/tree/v9.0.0" + }, + "time": "2024-03-25T20:55:39+00:00" + }, { "name": "defuse/php-encryption", "version": "v2.4.0", diff --git a/resources/views/theme/backend/adminlte/home.blade.php b/resources/views/theme/backend/adminlte/home.blade.php index 925cd5f..ac84e81 100644 --- a/resources/views/theme/backend/adminlte/home.blade.php +++ b/resources/views/theme/backend/adminlte/home.blade.php @@ -121,18 +121,4 @@ -@endsection - -@section('page-scripts') - -@append \ No newline at end of file +@endsection \ No newline at end of file diff --git a/resources/views/theme/backend/adminlte/r/invoice/widgets/due.blade.php b/resources/views/theme/backend/adminlte/r/invoice/widgets/due.blade.php index 4a60296..d479a11 100644 --- a/resources/views/theme/backend/adminlte/r/invoice/widgets/due.blade.php +++ b/resources/views/theme/backend/adminlte/r/invoice/widgets/due.blade.php @@ -41,8 +41,11 @@ -@section('page-scripts') +@section('page-styles') @css(datatables,bootstrap4|rowgroup) +@append + +@section('page-scripts') @js(datatables,bootstrap4|rowgroup) +@append \ No newline at end of file