From 7c5369203c85d15a61c8fb51b1e23f56e5c26ac7 Mon Sep 17 00:00:00 2001 From: Deon George Date: Fri, 1 Oct 2021 14:59:04 +1000 Subject: [PATCH] Optimise charge table, implemented charge recording, optimised payment recording --- app/Http/Controllers/AdminController.php | 55 ++- .../ResellerServicesController.php | 13 +- app/Models/Account.php | 6 + app/Models/Charge.php | 56 ++- app/Models/InvoiceItem.php | 24 +- app/Models/Payment.php | 7 +- app/Models/PaymentItem.php | 5 + .../2021_09_30_132207_rework_charges.php | 49 +++ .../adminlte/a/charge/addedit.blade.php | 345 ++++++++++++++++++ .../adminlte/a/charge/unprocessed.blade.php | 66 ++++ .../a/charge/widgets/pending.blade.php | 37 ++ .../adminlte/a/payment/addedit.blade.php | 38 +- .../adminlte/a/payment/unapplied.blade.php | 5 - .../a/payment/widgets/invoices.blade.php | 2 +- .../layouts/partials/sidebarmenu.blade.php | 60 ++- routes/api.php | 2 + routes/web.php | 9 + 17 files changed, 731 insertions(+), 48 deletions(-) create mode 100644 database/migrations/2021_09_30_132207_rework_charges.php create mode 100644 resources/views/theme/backend/adminlte/a/charge/addedit.blade.php create mode 100644 resources/views/theme/backend/adminlte/a/charge/unprocessed.blade.php create mode 100644 resources/views/theme/backend/adminlte/a/charge/widgets/pending.blade.php diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php index fd26e02..066d756 100644 --- a/app/Http/Controllers/AdminController.php +++ b/app/Http/Controllers/AdminController.php @@ -4,8 +4,9 @@ namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Auth; -use App\Models\{Account,Payment,PaymentItem,Service,SiteDetail}; +use App\Models\{Account,Charge,InvoiceItem,Payment,PaymentItem,Service,SiteDetail}; class AdminController extends Controller { @@ -14,6 +15,54 @@ class AdminController extends Controller return View('a.service',['o'=>$o]); } + public function charge_addedit(Request $request,Charge $o) + { + if ($request->post()) { + $request->validate([ + 'account_id' => 'required|exists:accounts,id', + 'charge_date' => 'required|date', + 'service_id' => 'required|exists:ab_service,id', + 'quantity' => 'required|numeric|not_in:0', + 'amount' => 'required|numeric|min:0.01', + 'sweep_type' => 'required|numeric|in:'.implode(',',array_keys(Charge::sweep)), + 'type' => 'required|numeric|in:'.implode(',',array_keys(InvoiceItem::type)), + 'taxable' => 'nullable|boolean', + 'description' => 'nullable|string|max:128', + ]); + + if (! $o->exists) { + $o->site_id = config('SITE')->site_id; + $o->user_id = Auth::id(); + $o->active = TRUE; + } + + $o->forceFill($request->only(['account_id','charge_date','service_id','quantity','amount','sweep_type','type','taxable','description'])); + $o->save(); + + return redirect()->back() + ->with('success','Charge recorded: '.$o->id); + } + + return view('a.charge.addedit') + ->with('o',$o); + } + + public function charge_pending_account(Request $request,Account $o) + { + return view('a.charge.widgets.pending') + ->with('list',$o->charges->where('active',TRUE)->where('processed',NULL)->except($request->exclude)); + } + + /** + * List unprocessed charges + * + * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View + */ + public function charge_unprocessed() + { + return view('a.charge.unprocessed'); + } + /** * Record payments on an account. * @@ -64,13 +113,13 @@ class AdminController extends Controller $oo->invoice_id = $id; } - $oo->alloc_amt = $amount; + $oo->alloc_amt = ($oo->invoice->due >= 0) && ($oo->invoice->due-$amount >= 0) ? $amount : 0; $oo->site_id = config('SITE')->site_id; $o->items()->save($oo); } return redirect()->back() - ->with('success','Payment recorded'); + ->with('success','Payment recorded: '.$o->id); } return view('a.payment.addedit') diff --git a/app/Http/Controllers/ResellerServicesController.php b/app/Http/Controllers/ResellerServicesController.php index 7e523f0..bbedf5d 100644 --- a/app/Http/Controllers/ResellerServicesController.php +++ b/app/Http/Controllers/ResellerServicesController.php @@ -2,7 +2,10 @@ namespace App\Http\Controllers; -use Auth; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; + +use App\Models\Account; class ResellerServicesController extends Controller { @@ -21,6 +24,14 @@ class ResellerServicesController extends Controller return ['data'=>Auth::user()->all_clients()->values()]; } + public function services(Request $request,Account $o) + { + return $o->services + ->filter(function($item) use ($request) { + return $item->active || ($item->id == $request->include); + }); + } + public function service_inactive() { return ['data'=>Auth::user()->all_client_service_inactive()->values()]; diff --git a/app/Models/Account.php b/app/Models/Account.php index 22b2226..465b930 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -16,6 +16,7 @@ use App\Traits\NextKey; * Attributes for accounts: * + lid: : Local ID for account * + sid: : System ID for account + * + name: : Account Name * * @package App\Models */ @@ -48,6 +49,11 @@ class Account extends Model implements IDs /* RELATIONS */ + public function charges() + { + return $this->hasMany(Charge::class); + } + /** * Return the country the user belongs to */ diff --git a/app/Models/Charge.php b/app/Models/Charge.php index 5d997d7..8ea4929 100644 --- a/app/Models/Charge.php +++ b/app/Models/Charge.php @@ -3,15 +3,67 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Arr; +/** + * CLEANUP NOTES: + * + Charge Date should not be null + * + Attributes should be a collection array + * + type should not be null + */ class Charge extends Model { - protected $table = 'ab_charge'; - protected $dates = ['date_charge']; + const CREATED_AT = 'date_orig'; + const UPDATED_AT = 'date_last'; + + protected $dates = ['charge_date']; public $dateFormat = 'U'; + public const sweep = [ + // 0 => 'Daily', + // 1 => 'Weekly', + // 2 => 'Monthly', + // 3 => 'Quarterly', + // 4 => 'Semi-Annually', + // 5 => 'Annually', + 6 => 'Service Rebill', + ]; + + /* RELATIONS */ + + public function account() + { + return $this->belongsTo(Account::class); + } + + public function service() + { + return $this->belongsTo(Service::class); + } + + /* SCOPES */ + + public function scopeUnprocessed($query) + { + return $query + ->where('active',TRUE) + ->whereNotNull('charge_date') + ->whereNotNull('type') + ->where(function($q) { + return $q->where('processed',FALSE) + ->orWhereNull('processed'); + }); + } + + /* ATTRIBUTES */ + public function getNameAttribute() { return sprintf('%s %s',$this->description,$this->getAttribute('attributes') ? join('|',unserialize($this->getAttribute('attributes'))) : ''); } + + public function getTypeAttribute($value) + { + return Arr::get(InvoiceItem::type,$value); + } } \ No newline at end of file diff --git a/app/Models/InvoiceItem.php b/app/Models/InvoiceItem.php index bd6d40c..bd6f302 100644 --- a/app/Models/InvoiceItem.php +++ b/app/Models/InvoiceItem.php @@ -37,6 +37,22 @@ class InvoiceItem extends Model // Array of items that can be updated with PushNew protected $pushable = ['taxes']; + public const type = [ + 1 => 'Hardware', // * + 2 => 'Service Relocation Fee', // * Must have corresponding SERVICE_ID + 3 => 'Service Change', // * Must have corresponding SERVICE_ID + 4 => 'Service Connection', // * Must have corresponding SERVICE_ID + 6 => 'Service Cancellation', // * Must have corresponding SERVICE_ID + 7 => 'Extra Product/Service Charge', // * Service Billing in advance, Must have corresponding SERVICE_ID + 8 => 'Product Addition', // * Additional Product Customisation, Must have corresponding SERVICE_ID + 120 => 'Credit/Debit Transfer', // * SERVICE_ID is NULL, MODULE_ID is NULL, MODULE_REF is NULL : INVOICE_ID is NOT NULL + 123 => 'Shipping', + 124 => 'Late Payment Fee', // * SERVICE_ID is NULL, MODULE_ID = CHECKOUT MODULE, + 125 => 'Payment Fee', // * SERVICE_ID is NULL, MODULE_ID = CHECKOUT MODULE, MODULE_REF = CHECKOUT NAME + 126 => 'Other', // * MODEL_ID should be a module + 127 => 'Rounding', // * SERVICE_ID is NULL, MODULE_ID is NULL, MODULE_REF is NULL + ]; + /* RELATIONS */ public function invoice() @@ -98,6 +114,7 @@ class InvoiceItem extends Model public function getItemTypeNameAttribute() { + // @todo use self::type $types = [ 1=>'Hardware', // * 2=>'Service Relocation Fee', // * Must have corresponding SERVICE_ID @@ -118,8 +135,11 @@ class InvoiceItem extends Model { // * Line Charge Topic on Invoice. case 0: - return sprintf('%s [%s]','Product/Service', - $this->date_start == $this->date_stop ? $this->date_start->format('Y-m-d') : sprintf('%s -> %s',$this->date_start->format('Y-m-d'),$this->date_stop->format('Y-m-d'))); + if ($this->date_start) + return sprintf('%s [%s]','Product/Service', + (($this->date_start == $this->date_stop) || (! $this->date_stop)) ? $this->date_start->format('Y-m-d') : sprintf('%s -> %s',$this->date_start->format('Y-m-d'),$this->date_stop->format('Y-m-d'))); + else + return 'Product/Service'; // * Excess Service Item, of item 0, must have corresponding SERVICE_ID case 5: diff --git a/app/Models/Payment.php b/app/Models/Payment.php index b4c92c8..ad3eba2 100644 --- a/app/Models/Payment.php +++ b/app/Models/Payment.php @@ -3,11 +3,11 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; - -use App\Interfaces\IDs; use Illuminate\Support\Facades\DB; use Leenooks\Traits\ScopeActive; -use App\Traits\{NextKey,PushNew}; + +use App\Interfaces\IDs; +use App\Traits\PushNew; /** * Class Payment @@ -74,7 +74,6 @@ class Payment extends Model implements IDs public function scopeUnapplied($query) { - //DB::enableQueryLog(); return $query ->select(['payments.id','payment_date','account_id','checkout_id','total_amt',DB::raw("SUM(alloc_amt) as allocated")]) ->leftJoin('payment_items',['payment_items.payment_id'=>'payments.id']) diff --git a/app/Models/PaymentItem.php b/app/Models/PaymentItem.php index 894f9a8..2c25d18 100644 --- a/app/Models/PaymentItem.php +++ b/app/Models/PaymentItem.php @@ -17,6 +17,11 @@ class PaymentItem extends Model /* RELATIONS */ + public function invoice() + { + return $this->belongsTo(Invoice::class); + } + public function payment() { return $this->belongsTo(Payment::class); } diff --git a/database/migrations/2021_09_30_132207_rework_charges.php b/database/migrations/2021_09_30_132207_rework_charges.php new file mode 100644 index 0000000..9942bc8 --- /dev/null +++ b/database/migrations/2021_09_30_132207_rework_charges.php @@ -0,0 +1,49 @@ +dropForeign('fk_chg_acc'); + $table->dropForeign('fk_chg_pdt'); + $table->dropForeign('fk_chg_svc'); + $table->dropIndex('fk_chg_acc_idx'); + $table->dropIndex('fk_chg_svc_idx'); + $table->dropIndex('fk_chg_pdt_idx'); + $table->dropPrimary(['id','account_id','site_id']); + }); + + DB::statement('ALTER TABLE ab_charge RENAME TO charges'); + DB::statement('ALTER TABLE charges RENAME COLUMN date_charge TO charge_date'); + DB::statement('ALTER TABLE charges MODIFY COLUMN id INT auto_increment'); + + Schema::table('charges', function (Blueprint $table) { + $table->unique(['id','account_id','site_id']); + $table->foreign(['account_id','site_id'])->references(['id','site_id'])->on('accounts'); + $table->foreign(['service_id','site_id'])->references(['id','site_id'])->on('ab_service'); + $table->foreign(['product_id','site_id'])->references(['id','site_id'])->on('ab_product'); + $table->integer('user_id')->unsigned()->nullable(); + $table->foreign(['user_id','site_id'])->references(['id','site_id'])->on('users'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + abort(500,'cant go back'); + } +} diff --git a/resources/views/theme/backend/adminlte/a/charge/addedit.blade.php b/resources/views/theme/backend/adminlte/a/charge/addedit.blade.php new file mode 100644 index 0000000..b940340 --- /dev/null +++ b/resources/views/theme/backend/adminlte/a/charge/addedit.blade.php @@ -0,0 +1,345 @@ +@extends('adminlte::layouts.app') + +@section('htmlheader_title') + Charge {{ $o->id ? '#'. $o->id : '' }} +@endsection +@section('page_title') + Charge +@endsection + +@section('contentheader_title') + Record Charge +@endsection +@section('contentheader_description') +@endsection + +@section('main-content') +
+
+
+
+

Record Charge {{ $o->id ? '#'. $o->id : '' }}

+ @if(session()->has('success')) + {{ session()->get('success') }} + @endif +
+ +
+
+ @csrf + +
+ +
+
+ +
+
+ +
+ + + @error('charge_date') + {{ $message }} + @else + Charge Date is required. + @enderror + +
+ Date Payment Received. +
+
+ + +
+
+ +
+
+ +
+ + + @error('quantity') + {{ $message }} + @else + Quantity is required. + @enderror + +
+
+
+
+ +
+ +
+
+ +
+
+ +
+ + + + @error('account_id') + {{ $message }} + @else + Account is required. + @enderror + +
+ Account to add charge to. +
+
+ + +
+
+ +
+
+ +
+ + + @error('sweep_type') + {{ $message }} + @else + Sweep Type is required. + @enderror + +
+ When to add the charge to an invoice. +
+
+ + +
+
+ +
+ exists ? $o->taxable : 1) ? 'checked' : '' }}> +
+
+
+ + +
+
+ +
+
+ +
+ + + @error('amount') + {{ $message }} + @else + Amount is required. + @enderror + +
+ Amount (ex Tax). +
+
+
+ +
+ +
+
+ +
+
+ +
+ + + + + @error('service_id') + {{ $message }} + @else + Service is required. + @enderror + +
+ **Service inactive. +
+
+ + +
+
+ +
+
+ +
+ + + @error('type') + {{ $message }} + @else + Type is required. + @enderror + +
+ Charge type. +
+
+ + +
+ +
+
+ +
+ +
+ Total (ex Tax). +
+
+ +
+ +
+
+ +
+
+ +
+ + + + @error('description') + {{ $message }} + @enderror + +
+
+
+
+ +
+
+ Cancel + @can('wholesaler') + + @endcan +
+
+
+
+
+
+ +
+
+
+
+@endsection + +@section('page-scripts') + @css('//cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/css/select2.min.css','select-css') + @js('//cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.min.js','select-js') + @js('/select2/fix-autofocus.js','select-fix-js','select-js') + @css('/select2/fix-select-height.css','select-fix-css','select-css') + + +@append diff --git a/resources/views/theme/backend/adminlte/a/charge/unprocessed.blade.php b/resources/views/theme/backend/adminlte/a/charge/unprocessed.blade.php new file mode 100644 index 0000000..ac26827 --- /dev/null +++ b/resources/views/theme/backend/adminlte/a/charge/unprocessed.blade.php @@ -0,0 +1,66 @@ +@extends('adminlte::layouts.app') + +@section('htmlheader_title') + Unprocessed Charges +@endsection +@section('page_title') + Unprocessed +@endsection + +@section('contentheader_title') + Unprocessed Charges +@endsection +@section('contentheader_description') +@endsection + +@section('main-content') +
+
+
+
+ + + + + + + + + + + + + + + @foreach(\App\Models\Charge::unprocessed()->with(['account.user','service'])->get() as $o) + + + + + + + + + + @endforeach + +
IDDate CreatedDate ChargeAccountServiceDescriptionTotal
{{ $o->id }}{{ $o->charge_date->format('Y-m-d') }}{{ $o->date_orig->format('Y-m-d') }}{{ $o->account->name }}{{ $o->service->name_short }}{{ $o->description }}{{ number_format($o->quantity*$o->amount,2) }}
+
+
+
+
+@endsection + +@section('page-scripts') + @css('//cdn.datatables.net/1.10.25/css/dataTables.bootstrap4.min.css','jq-dt-css') + @js('//cdn.datatables.net/1.10.25/js/jquery.dataTables.min.js','jq-dt-js') + @js('//cdn.datatables.net/1.10.25/js/dataTables.bootstrap4.min.js','jq-dt-bs5-js','jq-dt-js') + + +@append \ No newline at end of file diff --git a/resources/views/theme/backend/adminlte/a/charge/widgets/pending.blade.php b/resources/views/theme/backend/adminlte/a/charge/widgets/pending.blade.php new file mode 100644 index 0000000..1e4376c --- /dev/null +++ b/resources/views/theme/backend/adminlte/a/charge/widgets/pending.blade.php @@ -0,0 +1,37 @@ +@if(($x=$list)->count()) +
+
+

Pending Charges

+
+ +
+ + + + + + + + + + + + + + + @foreach ($x as $co) + + + + + + + + + + @endforeach + +
IDDate CreatedDate ChargeServiceTypeDescriptionTotal
{{ $co->id }}{{ $co->date_orig->format('Y-m-d') }}{{ $co->charge_date->format('Y-m-d') }}{{ $co->service->sid }}{{ $co->type }}{{ $co->description }}{{ number_format($co->quantity*$co->amount,2) }}
+
+
+@endif \ No newline at end of file diff --git a/resources/views/theme/backend/adminlte/a/payment/addedit.blade.php b/resources/views/theme/backend/adminlte/a/payment/addedit.blade.php index 0a45982..00ae4fe 100644 --- a/resources/views/theme/backend/adminlte/a/payment/addedit.blade.php +++ b/resources/views/theme/backend/adminlte/a/payment/addedit.blade.php @@ -30,6 +30,7 @@ @csrf
+
@@ -50,6 +51,7 @@
+
@@ -72,6 +74,7 @@
+
@@ -97,6 +100,7 @@
+
@@ -119,6 +123,7 @@
+
@@ -129,7 +134,7 @@ @@ -147,13 +152,14 @@
+
- +
@@ -185,14 +191,10 @@ @endsection @section('page-scripts') - @css('//cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css','s2-css') - @js('//cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js','s2-js','jquery') - - + @css('//cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/css/select2.min.css','select-css') + @js('//cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.min.js','select-js') + @js('/select2/fix-autofocus.js','select-fix-js','select-js') + @css('/select2/fix-select-height.css','select-fix-css','select-css') @append diff --git a/resources/views/theme/backend/adminlte/a/payment/unapplied.blade.php b/resources/views/theme/backend/adminlte/a/payment/unapplied.blade.php index c632f3e..e9eb661 100644 --- a/resources/views/theme/backend/adminlte/a/payment/unapplied.blade.php +++ b/resources/views/theme/backend/adminlte/a/payment/unapplied.blade.php @@ -61,11 +61,6 @@ $(document).ready(function() { $('#unapplied_payments').DataTable( { order: [1,'desc'], - orderFixed: [1,'desc'] - }); - - $('#unapplied_payments tbody').on('click','tr', function () { - $(this).toggleClass('selected'); }); }); diff --git a/resources/views/theme/backend/adminlte/a/payment/widgets/invoices.blade.php b/resources/views/theme/backend/adminlte/a/payment/widgets/invoices.blade.php index ecaba7c..a3e4485 100644 --- a/resources/views/theme/backend/adminlte/a/payment/widgets/invoices.blade.php +++ b/resources/views/theme/backend/adminlte/a/payment/widgets/invoices.blade.php @@ -27,7 +27,7 @@ {{ number_format($io->total,2) }} {{ number_format($io->due,2) }} - + @endforeach diff --git a/resources/views/vendor/adminlte/layouts/partials/sidebarmenu.blade.php b/resources/views/vendor/adminlte/layouts/partials/sidebarmenu.blade.php index f2b7add..7044bee 100644 --- a/resources/views/vendor/adminlte/layouts/partials/sidebarmenu.blade.php +++ b/resources/views/vendor/adminlte/layouts/partials/sidebarmenu.blade.php @@ -23,27 +23,49 @@ @can('wholesaler') - - + + + @endcan @can('wholesaler') diff --git a/routes/api.php b/routes/api.php index 457079d..3992a17 100644 --- a/routes/api.php +++ b/routes/api.php @@ -18,6 +18,8 @@ use App\Http\Controllers\{AdminController, Route::group(['middleware'=>['auth:api','role:reseller']], function() { // Route::get('/r/agents','ResellerServicesController@agents'); Route::get('/r/accounts',[ResellerServicesController::class,'accounts']); + Route::get('/r/services/{o}',[ResellerServicesController::class,'services']) + ->where('o','[0-9]+'); // Route::get('/r/clients','ResellerServicesController@clients'); // Route::get('/r/service_inactive','ResellerServicesController@service_inactive'); Route::post('r/invoices/{o}',[AdminController::class,'pay_invoices']) diff --git a/routes/web.php b/routes/web.php index 6f59437..6918e90 100644 --- a/routes/web.php +++ b/routes/web.php @@ -44,6 +44,11 @@ Route::group(['middleware'=>['theme:adminlte-be','auth','role:wholesaler'],'pref // Route::post('service/{o}','AdminHomeController@service_update'); // Route::get('report/products','Wholesale\ReportController@products'); + // Charges + Route::match(['get','post'],'charge/addedit/{o?}',[AdminController::class,'charge_addedit']); + Route::get('charge/unprocessed',[AdminController::class,'charge_unprocessed']); + + // Payments Route::match(['get','post'],'payment/addedit/{o?}',[AdminController::class,'pay_addedit']); Route::get('payment/unapplied',[AdminController::class,'pay_unapplied']); @@ -65,6 +70,10 @@ Route::group(['middleware'=>['theme:adminlte-be','auth','role:reseller'],'prefix Route::group(['middleware'=>['theme:adminlte-be','auth','role:reseller'],'prefix'=>'report'],function() { Route::get('domain',[ServiceController::class,'domain_list']); }); + + // Charges on an account + Route::get('charges/{o}',[AdminController::class,'charge_pending_account']) + ->where('o','[0-9]+'); }); // Our User Routes