diff --git a/.gitlab-test.yml b/.gitlab-test.yml
index 88e157d..11d0ce8 100644
--- a/.gitlab-test.yml
+++ b/.gitlab-test.yml
@@ -1,5 +1,5 @@
test:
- image: ${CI_REGISTRY}/leenooks/php:8.0-fpm-ext-test
+ image: ${CI_REGISTRY}/leenooks/php:8.0-fpm-image-test
stage: test
diff --git a/app/Console/Commands/PaymentsEzypayImport.php b/app/Console/Commands/PaymentsEzypayImport.php
index 9a6d813..2e215db 100644
--- a/app/Console/Commands/PaymentsEzypayImport.php
+++ b/app/Console/Commands/PaymentsEzypayImport.php
@@ -66,7 +66,7 @@ class PaymentsEzypayImport extends Command
}
// Find the last payment logged
- $last = Carbon::createFromTimestamp(Payment::whereIN('checkout_id',$cos)->where('account_id',$ao->id)->max('date_payment'));
+ $last = Carbon::createFromTimestamp(Payment::whereIN('checkout_id',$cos)->where('account_id',$ao->id)->max('payment_date'));
$o = $poo->getDebits([
'customerId'=>$c->Id,
@@ -87,13 +87,13 @@ class PaymentsEzypayImport extends Command
$pd = Carbon::createFromFormat('Y-m-d?H:i:s.u',$p->Date);
$lp = $ao->payments->last();
- if ($lp AND (($pd == $lp->date_payment) OR ($p->Id == $lp->checkout_data)))
+ if ($lp AND (($pd == $lp->payment_date) OR ($p->Id == $lp->checkout_data)))
continue;
// New Payment
$po = new Payment;
$po->site_id = 1; // @todo
- $po->date_payment = $pd;
+ $po->payment_date = $pd;
$po->checkout_id = '999'; // @todo
$po->checkout_data = $p->Id;
$po->total_amt = $p->Amount;
diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php
index ee86eda..a959cb1 100644
--- a/app/Http/Controllers/AdminController.php
+++ b/app/Http/Controllers/AdminController.php
@@ -21,12 +21,12 @@ class AdminController extends Controller
* @param Payment $o
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|\Illuminate\Http\RedirectResponse
*/
- public function pay_add(Request $request,Payment $o)
+ public function pay_addedit(Request $request,Payment $o)
{
if ($request->post()) {
$validation = $request->validate([
'account_id' => 'required|exists:accounts,id',
- 'date_payment' => 'required|date',
+ 'payment_date' => 'required|date',
'checkout_id' => 'required|exists:ab_checkout,id',
'total_amt' => 'required|numeric|min:0.01',
'fees_amt' => 'nullable|numeric|lt:total_amt',
@@ -41,36 +41,64 @@ class AdminController extends Controller
'invoices.*.id' => 'nullable|exists:ab_invoice,id',
]);
- $oo = new Payment;
- $oo->forceFill($request->only(['account_id','date_payment','checkout_id','checkout_id','total_amt','fees_amt','source_id','pending','notes','ip']));
- $oo->site_id = config('SITE')->site_id;
- $oo->save();
+ if (! $o->exists) {
+ $o->forceFill($request->only(['account_id','payment_date','checkout_id','checkout_id','total_amt','fees_amt','source_id','pending','notes','ip']));
+ $o->site_id = config('SITE')->site_id;
+ $o->save();
+ }
foreach ($validation['invoices'] as $id => $amount) {
- $ooo = new PaymentItem;
- $ooo->invoice_id = $id;
- $ooo->alloc_amt = $amount;
- $ooo->site_id = config('SITE')->site_id;
- $oo->items()->save($ooo);
+
+ // See if we already have a payment item that we need to update
+ $items = $o->items->filter(function($item) use ($id) { return $item->invoice_id == $id; });
+
+ if ($items->count() == 1) {
+ $oo = $items->pop();
+
+ if (! $amount) {
+ $oo->delete();
+ continue;
+ }
+
+ } else {
+ $oo = new PaymentItem;
+ $oo->invoice_id = $id;
+ }
+
+ $oo->alloc_amt = $amount;
+ $oo->site_id = config('SITE')->site_id;
+ $o->items()->save($oo);
}
return redirect()->back()
->with('success','Payment recorded');
}
- return view('a.payment.add')
+ return view('a.payment.addedit')
->with('o',$o);
}
+ /**
+ * List unapplied payments
+ *
+ * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
+ */
+ public function pay_unapplied()
+ {
+ return view('a.payment.unapplied');
+ }
+
/**
* Show a list of invoices to apply payments to
*
+ * @param Request $request
* @param Account $o
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
*/
- public function pay_invoices(Account $o)
+ public function pay_invoices(Request $request,Account $o)
{
return view('a.payment.widgets.invoices')
+ ->with('pid',$request->pid)
->with('o',$o);
}
diff --git a/app/Http/Controllers/PaypalController.php b/app/Http/Controllers/PaypalController.php
index cc740a1..8c0fbcf 100644
--- a/app/Http/Controllers/PaypalController.php
+++ b/app/Http/Controllers/PaypalController.php
@@ -217,7 +217,7 @@ class PaypalController extends Controller
break;
}
- $po->date_payment = Carbon::parse($cap->create_time);
+ $po->payment_date = Carbon::parse($cap->create_time);
$po->checkout_id = $this->o->id;
$po->checkout_data = $cap->id;
diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php
index f1c4a96..eaffdb5 100644
--- a/app/Http/Controllers/SearchController.php
+++ b/app/Http/Controllers/SearchController.php
@@ -5,8 +5,9 @@ namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Gate;
-use App\Models\{Account,Invoice,Service,Service\Adsl,Service\Voip,User};
+use App\Models\{Account,Invoice,Payment,Service,Service\Adsl,Service\Voip,User};
class SearchController extends Controller
{
@@ -90,6 +91,16 @@ class SearchController extends Controller
$result->push(['name'=>sprintf('%s (%s)',$o->service_name,$o->service->sid),'value'=>'/u/service/'.$o->id,'category'=>'Domains']);
}
+ if (Gate::any(['wholesaler'],new Payment)) {
+ # Look for Payments
+ foreach (Payment::Search($request->input('term'))
+ ->whereIN('account_id',$accounts)
+ ->limit(10)->get() as $o)
+ {
+ $result->push(['name'=>sprintf('%s ($%s)',$o->id,number_format($o->total,2)),'value'=>'/a/payment/edit'.$o->id,'category'=>'Payments']);
+ }
+ }
+
return $result;
}
}
\ No newline at end of file
diff --git a/app/Models/Account.php b/app/Models/Account.php
index 3fbf273..22b2226 100644
--- a/app/Models/Account.php
+++ b/app/Models/Account.php
@@ -95,7 +95,7 @@ class Account extends Model implements IDs
*
* @param $query
* @param string $term
- * @return
+ * @return mixed
*/
public function scopeSearch($query,string $term)
{
diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php
index 96aeb20..ab5d28c 100644
--- a/app/Models/Invoice.php
+++ b/app/Models/Invoice.php
@@ -106,7 +106,7 @@ class Invoice extends Model implements IDs
*/
public function getDueAttribute(): float
{
- return sprintf('%4.'.$this->currency()->rounding.'f',$this->total-$this->paid);
+ return sprintf('%3.2f',$this->getTotalAttribute()-$this->getPaidAttribute());
}
/**
@@ -162,28 +162,27 @@ class Invoice extends Model implements IDs
*/
public function getPaidAttribute(): float
{
- if (! $this->_paid)
- $this->_paid = $this->currency()->round(
- $this->paymentitems
- ->filter(function($item) { return ! $item->payment->pending_status; })
- ->sum('alloc_amt'));
-
- return $this->_paid;
+ return $this->paymentitems
+ ->filter(function($item) { return ! $item->payment->pending_status; })
+ ->sum('alloc_amt');
}
/**
* Get the date that the invoice was paid in full.
- * We assume the last payment received pays it in full.
+ * We assume the last payment received pays it in full, if its fully paid.
*
* @return Carbon|null
*/
public function getPaidDateAttribute(): ?Carbon
{
+ if ($this->getDueAttribute())
+ return NULL;
+
$o = $this->payments
->filter(function($item) { return ! $item->pending_status; })
->last();
- return $o ? $o->date_payment : NULL;
+ return $o ? $o->payment_date : NULL;
}
/**
@@ -193,10 +192,9 @@ class Invoice extends Model implements IDs
*/
public function getPaidPendingAttribute(): float
{
- return $this->currency()->round(
- $this->paymentitems
- ->filter(function($item) { return $item->payment->pending_status; })
- ->sum('alloc_amt'));
+ return $this->paymentitems
+ ->filter(function($item) { return $item->payment->pending_status; })
+ ->sum('alloc_amt');
}
/**
@@ -238,7 +236,7 @@ class Invoice extends Model implements IDs
*/
public function getTotalSubAttribute(): float
{
- return sprintf('%3.'.$this->currency()->rounding.'f',$this->total-$this->tax_total);
+ return $this->items->where('active',TRUE)->sum('sub_total');
}
/**
@@ -259,29 +257,17 @@ class Invoice extends Model implements IDs
*/
public function getTotalTaxAttribute(): float
{
- if (! $this->_total_tax)
- foreach ($this->items as $o) {
- if ($o->active)
- $this->_total_tax += $this->currency()->round($o->tax);
- }
-
- return sprintf('%3.'.$this->currency()->rounding.'f',$this->_total_tax);
+ return $this->items->where('active',TRUE)->sum('tax');
}
/**
* Invoice total due
*
- * @return string
+ * @return float
*/
public function getTotalAttribute(): float
{
- if (! $this->_total)
- foreach ($this->items as $o) {
- if ($o->active)
- $this->_total += $this->currency()->round($o->total);
- }
-
- return sprintf('%3.'.$this->currency()->rounding.'f',$this->_total);
+ return $this->getTotalSubAttribute()+$this->getTotalTaxAttribute();
}
public function currency()
diff --git a/app/Models/InvoiceItem.php b/app/Models/InvoiceItem.php
index 9029bef..bd6d40c 100644
--- a/app/Models/InvoiceItem.php
+++ b/app/Models/InvoiceItem.php
@@ -10,6 +10,17 @@ use App\Traits\NextKey;
use App\Traits\PushNew;
use Leenooks\Carbon;
+/**
+ * Class Invoice Items
+ * Items that belong on an invoice
+ *
+ * Attributes for services:
+ * + date_start : Start date
+ * + date_stop : End date
+ * + sub_total : Value of item
+ * + tax : Total of all taxes
+ * + total : Total including taxes
+ */
class InvoiceItem extends Model
{
use NextKey,PushNew;
@@ -26,7 +37,7 @@ class InvoiceItem extends Model
// Array of items that can be updated with PushNew
protected $pushable = ['taxes'];
- private $_tax = 0;
+ /* RELATIONS */
public function invoice()
{
@@ -53,7 +64,7 @@ class InvoiceItem extends Model
return $this->hasMany(InvoiceItemTax::class);
}
- /** ATTRIBUTES **/
+ /* ATTRIBUTES */
/**
* Start date for the invoice item line
@@ -122,30 +133,37 @@ class InvoiceItem extends Model
}
}
- public function getSubTotalAttribute()
+ /**
+ * Sub total of item
+ *
+ * @return float
+ */
+ public function getSubTotalAttribute(): float
{
- return $this->quantity * $this->price_base;
+ return sprintf('%3.2f',$this->quantity * $this->price_base);
}
- public function getTaxAttribute()
+ /**
+ * Total of all taxes
+ *
+ * @return mixed
+ */
+ public function getTaxAttribute(): float
{
- if (! $this->_tax)
- {
- foreach ($this->taxes as $o)
- {
- $this->_tax += $o->amount;
- }
- }
-
- return $this->_tax;
+ return sprintf('%3.2f',$this->taxes->sum('amount'));
}
- public function getTotalAttribute()
+ /**
+ * Total including taxes
+ *
+ * @return float
+ */
+ public function getTotalAttribute(): float
{
- return $this->tax + $this->sub_total;
+ return sprintf('%3.2f',$this->getSubTotalAttribute()+$this->getTaxAttribute());
}
- /** FUNCTIONS **/
+ /* METHODS */
/**
* Add taxes to this record
diff --git a/app/Models/Payment.php b/app/Models/Payment.php
index 903e1fe..c329c36 100644
--- a/app/Models/Payment.php
+++ b/app/Models/Payment.php
@@ -5,6 +5,8 @@ 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};
/**
@@ -16,27 +18,26 @@ use App\Traits\{NextKey,PushNew};
* + payment_date : Date payment received
* + sid : System ID for payment
* + total : Payment total
+ * + balance : Remaining credit on payment
*
* @package App\Models
*/
class Payment extends Model implements IDs
{
- use NextKey,PushNew;
-
- const RECORD_ID = 'payment';
- public $incrementing = FALSE;
+ use PushNew;
const CREATED_AT = 'date_orig';
const UPDATED_AT = 'date_last';
- protected $table = 'ab_payment';
- protected $dates = ['date_payment'];
+ protected $dates = ['payment_date'];
protected $dateFormat = 'U';
- //protected $with = ['account.country.currency','items'];
// Array of items that can be updated with PushNew
protected $pushable = ['items'];
+ // Any balance below this we'll assume its all used.
+ private const threshold = 0.05;
+
/* RELATIONS */
public function account()
@@ -44,20 +45,49 @@ class Payment extends Model implements IDs
return $this->belongsTo(Account::class);
}
+ public function checkout()
+ {
+ return $this->belongsTo(Checkout::class);
+ }
+
public function items()
{
return $this->hasMany(PaymentItem::class);
}
- /* ATTRIBUTES */
+ /* SCOPES */
/**
+ * Search for a record
+ *
+ * @param $query
+ * @param string $term
* @return mixed
- * @deprecated use date_payment directly.
*/
- public function getDatePaidAttribute()
+ public function scopeSearch($query,string $term)
{
- return $this->date_payment->format('Y-m-d');
+ // Build our where clause
+ $query->where('id','like','%'.$term.'%');
+
+ return $query;
+ }
+
+ public function scopeUnapplied($query)
+ {
+ return $query
+ ->select([DB::raw('payment_id AS id'),'payment_date','account_id','checkout_id','total_amt',DB::raw("SUM(alloc_amt) as allocated")])
+ ->join('payment_items',['payment_items.payment_id'=>'payments.id'])
+ ->groupBy(['payment_id','payment_date','total_amt','account_id','checkout_id'])
+ ->having(DB::raw("ROUND(total_amt-allocated,2)"),'>',self::threshold);
+ }
+
+ /* ATTRIBUTES */
+
+ public function getBalanceAttribute(): float
+ {
+ $balance = $this->getTotalAttribute()-$this->items->sum('alloc_amt');
+
+ return ($balance < self::threshold) ? 0 : $balance;
}
/**
@@ -80,13 +110,8 @@ class Payment extends Model implements IDs
return sprintf('%02s-%04s#%s',$this->site_id,$this->account_id,$this->getLIDattribute());
}
- public function getTotalAttribute()
+ public function getTotalAttribute(): float
{
- return sprintf('%3.'.$this->currency()->rounding.'f',$this->total_amt);
- }
-
- public function currency()
- {
- return $this->account->country->currency;
+ return sprintf('%3.2f',$this->total_amt);
}
}
\ No newline at end of file
diff --git a/app/Models/PaymentItem.php b/app/Models/PaymentItem.php
index e5652b3..894f9a8 100644
--- a/app/Models/PaymentItem.php
+++ b/app/Models/PaymentItem.php
@@ -8,19 +8,29 @@ use App\Traits\{NextKey,PushNew};
class PaymentItem extends Model
{
- use NextKey,PushNew;
+ use PushNew;
const RECORD_ID = 'payment_item';
- public $incrementing = FALSE;
protected $dateFormat = 'U';
const CREATED_AT = 'date_orig';
const UPDATED_AT = 'date_last';
- protected $table = 'ab_payment_item';
-
/* RELATIONS */
public function payment() {
return $this->belongsTo(Payment::class);
}
+
+ /* ATTRIBUTES */
+
+ /**
+ * If our amount is negative, and invoice_id is null, then this is a reversal.
+ *
+ * @param $value
+ * @return float
+ */
+ public function getAllocAmtAttribute($value): float
+ {
+ return (is_null($this->invoice_id) && $value < 0) ? -$value : $value;
+ }
}
\ No newline at end of file
diff --git a/app/Models/User.php b/app/Models/User.php
index e690175..fbddc5b 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -241,7 +241,7 @@ class User extends Authenticatable
public function getPaymentHistoryAttribute()
{
return $this->payments
- ->sortBy('date_payment')
+ ->sortBy('payment_date')
->reverse();
}
@@ -367,6 +367,8 @@ class User extends Authenticatable
$result->push($o);
}
+ $result->load('user.accounts');
+
return $result;
}
@@ -623,7 +625,7 @@ class User extends Authenticatable
->select([
DB::raw('ab_payment.id AS id'),
'date_orig',
- 'date_payment',
+ 'payment_date',
'total_amt',
//'fees_amt',
DB::raw('total_amt-allocate AS balance'),
diff --git a/database/migrations/2021_07_23_164158_rework_payments.php b/database/migrations/2021_07_23_164158_rework_payments.php
new file mode 100644
index 0000000..7587c73
--- /dev/null
+++ b/database/migrations/2021_07_23_164158_rework_payments.php
@@ -0,0 +1,56 @@
+dropUnique('UNIQUE');
+ $table->dropForeign('fk_pi_pay');
+ $table->dropPrimary(['id','payment_id','site_id']);
+ $table->dropIndex('fk_pi_pay_idx');
+ });
+
+ DB::statement('ALTER TABLE ab_payment_item RENAME TO payment_items');
+
+
+ Schema::table('ab_payment', function (Blueprint $table) {
+ $table->dropPrimary(['id','checkout_id','account_id','site_id']);
+ $table->dropUnique('UNIQUE');
+ });
+
+ DB::statement('ALTER TABLE ab_payment RENAME TO payments');
+
+ DB::statement('ALTER TABLE payment_items ADD PRIMARY KEY (id,site_id), MODIFY COLUMN id bigint auto_increment');
+ DB::statement('ALTER TABLE payments ADD PRIMARY KEY (id,site_id), MODIFY COLUMN id bigint auto_increment');
+ DB::statement('ALTER TABLE payments RENAME COLUMN date_payment TO payment_date');
+
+ Schema::table('payment_items', function (Blueprint $table) {
+ $table->unique(['site_id','payment_id','invoice_id']);
+ $table->foreign(['payment_id','site_id'])->references(['id','site_id'])->on('payments');
+ });
+
+ Schema::table('payments', function (Blueprint $table) {
+ $table->unique(['site_id','id']);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ abort(500,'cant go back');
+ }
+}
diff --git a/resources/views/theme/backend/adminlte/a/payment/add.blade.php b/resources/views/theme/backend/adminlte/a/payment/addedit.blade.php
similarity index 82%
rename from resources/views/theme/backend/adminlte/a/payment/add.blade.php
rename to resources/views/theme/backend/adminlte/a/payment/addedit.blade.php
index d43ee59..6ea5c87 100644
--- a/resources/views/theme/backend/adminlte/a/payment/add.blade.php
+++ b/resources/views/theme/backend/adminlte/a/payment/addedit.blade.php
@@ -32,14 +32,14 @@
-
+
-
+
Account to add payment to.
+
+
-
-
+
+
@error('invoices')
@@ -193,6 +203,7 @@
dataType: 'html',
cache: false,
url: '{{ url('api/r/invoices') }}'+'/'+account,
+ data: {pid:{{ $o->id }}},
timeout: 2000,
error: function(x) {
spinner.toggleClass('d-none').toggleClass('fa-spin');
diff --git a/resources/views/theme/backend/adminlte/a/payment/unapplied.blade.php b/resources/views/theme/backend/adminlte/a/payment/unapplied.blade.php
new file mode 100644
index 0000000..c632f3e
--- /dev/null
+++ b/resources/views/theme/backend/adminlte/a/payment/unapplied.blade.php
@@ -0,0 +1,72 @@
+@extends('adminlte::layouts.app')
+
+@section('htmlheader_title')
+ Unapplied Payments
+@endsection
+@section('page_title')
+ Unapplied
+@endsection
+
+@section('contentheader_title')
+ Unapplied Payments
+@endsection
+@section('contentheader_description')
+@endsection
+
+@section('main-content')
+
+
+
+
+
+
+
+ ID |
+ Date Paid |
+ Account |
+ Method |
+ Total |
+ Balance |
+ Invoices |
+
+
+
+
+ @foreach(\App\Models\Payment::unapplied()->with(['account.user','checkout','items'])->get() as $o)
+ @if (! $o->balance) @continue @endif
+
+ {{ $o->id }} |
+ {{ $o->payment_date->format('Y-m-d') }} |
+ {{ $o->account->name }} |
+ {{ $o->checkout->name }} |
+ {{ number_format($o->total_amt,2) }} |
+ {{ number_format($o->balance,2) }} |
+ {!! $o->items->pluck('invoice_id')->map(function($item) { return sprintf('%d',url('u/invoice',$item),$item); })->join(', ') !!} |
+
+ @endforeach
+
+
+
+
+
+
+@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/payment/widgets/invoices.blade.php b/resources/views/theme/backend/adminlte/a/payment/widgets/invoices.blade.php
index ffd1036..3877550 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
@@ -1,6 +1,11 @@