From 1263e6a1db92a92cb817b627d63dd4f4adb76ebd Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sat, 17 Feb 2024 18:24:26 +1100 Subject: [PATCH 1/6] Add company setting --- app/DataMapper/CompanySettings.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/DataMapper/CompanySettings.php b/app/DataMapper/CompanySettings.php index 3079a50988c3..d2241f95b2f5 100644 --- a/app/DataMapper/CompanySettings.php +++ b/app/DataMapper/CompanySettings.php @@ -495,7 +495,10 @@ class CompanySettings extends BaseSettings public $show_pdfhtml_on_mobile = true; + public $use_unapplied_payment = 'off'; //always, option, off //@implemented + public static $casts = [ + 'use_unapplied_payment' => 'string', 'show_pdfhtml_on_mobile' => 'bool', 'payment_email_all_contacts' => 'bool', 'statement_design_id' => 'string', From 32b82f5bfe6d695a7a5afdf4a90f553948a2a4ba Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sat, 17 Feb 2024 18:41:40 +1100 Subject: [PATCH 2/6] Working on adding unapplied payments into autobill sequence --- app/Services/Invoice/AutoBillInvoice.php | 87 +++++++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/app/Services/Invoice/AutoBillInvoice.php b/app/Services/Invoice/AutoBillInvoice.php index 73eac355eefe..1091d1874825 100644 --- a/app/Services/Invoice/AutoBillInvoice.php +++ b/app/Services/Invoice/AutoBillInvoice.php @@ -31,6 +31,8 @@ class AutoBillInvoice extends AbstractService private Client $client; private array $used_credit = []; + + private array $used_unapplied = []; /*Specific variable for partial payments */ private bool $is_partial_amount = false; @@ -66,6 +68,10 @@ class AutoBillInvoice extends AbstractService $this->applyCreditPayment(); } + if($this->client->getSetting('use_unapplied_payment') != 'off') { + $this->applyUnappliedPayment(); + } + //If this returns true, it means a partial invoice amount was paid as a credit and there is no further balance payable if ($this->is_partial_amount && $this->invoice->partial == 0) { return; @@ -162,6 +168,11 @@ class AutoBillInvoice extends AbstractService } } + private function finalizePaymentUsingUnapplied() + { + + } + /** * If the credits on file cover the invoice amount * the we create a matching payment using credits only @@ -243,6 +254,80 @@ class AutoBillInvoice extends AbstractService ->setCalculatedStatus() ->save(); } + + /** + * If the client has unapplied payments on file + * we will use these prior to charging a + * payment method on file. + * + * This needs to be wrapped in a transaction. + * + * @return self + */ + private function applyUnappliedPayment(): self + { + $unapplied_payments = Payment::query()->where('client_id', $this->client->id) + ->where('status_id', Payment::STATUS_COMPLETED) + ->where('is_deleted', false) + ->where('amount', '>', 'applied') + ->where('amount', '>', 0) + ->orderBy('created_at') + ->get(); + + $available_unapplied_balance = $unapplied_payments->sum('amount') - $unapplied_payments->sum('applied'); + + nlog("available unapplied balance = {$available_unapplied_balance}"); + + if ((int) $available_unapplied_balance == 0) { + return $this; + } + + if ($this->invoice->partial > 0) { + $this->is_partial_amount = true; + } + + $this->used_unapplied = []; + + foreach ($unapplied_payments as $key => $payment) { + $payment_balance = $payment->amount - $payment->applied; + if ($this->is_partial_amount) { + //more than needed + if ($payment_balance > $this->invoice->partial) { + $this->used_unapplied[$key]['payment_id'] = $payment->id; + $this->used_unapplied[$key]['amount'] = $this->invoice->partial; + $this->invoice->balance -= $this->invoice->partial; + $this->invoice->paid_to_date += $this->invoice->partial; + $this->invoice->partial = 0; + break; + } else { + $this->used_unapplied[$key]['payment_id'] = $payment->id; + $this->used_unapplied[$key]['amount'] = $payment_balance; + $this->invoice->partial -= $payment_balance; + $this->invoice->balance -= $payment_balance; + $this->invoice->paid_to_date += $payment_balance; + } + } else { + //more than needed + if ($payment_balance > $this->invoice->balance) { + $this->used_unapplied[$key]['payment_id'] = $payment->id; + $this->used_unapplied[$key]['amount'] = $this->invoice->balance; + $this->invoice->paid_to_date += $this->invoice->balance; + $this->invoice->balance = 0; + + break; + } else { + $this->used_unapplied[$key]['payment_id'] = $payment->id; + $this->used_unapplied[$key]['amount'] = $payment_balance; + $this->invoice->balance -= $payment_balance; + $this->invoice->paid_to_date += $payment_balance; + } + } + } + + $this->finalizePaymentUsingUnapplied(); + + return $this; + } /** * Applies credits to a payment prior to push @@ -260,7 +345,7 @@ class AutoBillInvoice extends AbstractService $available_credit_balance = $available_credits->sum('balance'); - info("available credit balance = {$available_credit_balance}"); + nlog("available credit balance = {$available_credit_balance}"); if ((int) $available_credit_balance == 0) { return $this; From 66fd68cf9356f14a5d6ccfd5b063196a473b1690 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sat, 17 Feb 2024 19:03:20 +1100 Subject: [PATCH 3/6] Refactor for payment processing --- app/Services/Invoice/AutoBillInvoice.php | 48 +++++++++--------------- 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/app/Services/Invoice/AutoBillInvoice.php b/app/Services/Invoice/AutoBillInvoice.php index 1091d1874825..dd0da025ab99 100644 --- a/app/Services/Invoice/AutoBillInvoice.php +++ b/app/Services/Invoice/AutoBillInvoice.php @@ -22,6 +22,8 @@ use App\Models\Invoice; use App\Models\Payment; use App\Models\PaymentHash; use App\Models\PaymentType; +use App\Repositories\CreditRepository; +use App\Repositories\PaymentRepository; use App\Services\AbstractService; use App\Utils\Ninja; use Illuminate\Support\Str; @@ -32,8 +34,6 @@ class AutoBillInvoice extends AbstractService private array $used_credit = []; - private array $used_unapplied = []; - /*Specific variable for partial payments */ private bool $is_partial_amount = false; @@ -168,11 +168,6 @@ class AutoBillInvoice extends AbstractService } } - private function finalizePaymentUsingUnapplied() - { - - } - /** * If the credits on file cover the invoice amount * the we create a matching payment using credits only @@ -228,8 +223,6 @@ class AutoBillInvoice extends AbstractService ->client ->service() ->updateBalanceAndPaidToDate($amount * -1, $amount) - // ->updateBalance($amount * -1) - // ->updatePaidToDate($amount) ->adjustCreditBalance($amount * -1) ->save(); @@ -286,45 +279,40 @@ class AutoBillInvoice extends AbstractService $this->is_partial_amount = true; } - $this->used_unapplied = []; + $payment_repo = new PaymentRepository(new CreditRepository()); + foreach ($unapplied_payments as $key => $payment) { $payment_balance = $payment->amount - $payment->applied; + if ($this->is_partial_amount) { //more than needed if ($payment_balance > $this->invoice->partial) { - $this->used_unapplied[$key]['payment_id'] = $payment->id; - $this->used_unapplied[$key]['amount'] = $this->invoice->partial; - $this->invoice->balance -= $this->invoice->partial; - $this->invoice->paid_to_date += $this->invoice->partial; - $this->invoice->partial = 0; + $payload = ['invoices' => [['invoice_id' => $this->invoice->id,'amount' => $this->invoice->partial]]]; + $payment_repo->save($payload, $payment); break; } else { - $this->used_unapplied[$key]['payment_id'] = $payment->id; - $this->used_unapplied[$key]['amount'] = $payment_balance; - $this->invoice->partial -= $payment_balance; - $this->invoice->balance -= $payment_balance; - $this->invoice->paid_to_date += $payment_balance; + $payload = ['invoices' => [['invoice_id' => $this->invoice->id,'amount' => $payment_balance]]]; + $payment_repo->save($payload, $payment); } } else { //more than needed if ($payment_balance > $this->invoice->balance) { - $this->used_unapplied[$key]['payment_id'] = $payment->id; - $this->used_unapplied[$key]['amount'] = $this->invoice->balance; - $this->invoice->paid_to_date += $this->invoice->balance; - $this->invoice->balance = 0; + + $payload = ['invoices' => [['invoice_id' => $this->invoice->id,'amount' => $this->invoice->balance]]]; + $payment_repo->save($payload, $payment); break; } else { - $this->used_unapplied[$key]['payment_id'] = $payment->id; - $this->used_unapplied[$key]['amount'] = $payment_balance; - $this->invoice->balance -= $payment_balance; - $this->invoice->paid_to_date += $payment_balance; + + $payload = ['invoices' => [['invoice_id' => $this->invoice->id,'amount' => $payment_balance]]]; + $payment_repo->save($payload, $payment); + } } - } - $this->finalizePaymentUsingUnapplied(); + $this->invoice = $this->invoice->fresh(); + } return $this; } From 97accc814249d433a850bbf4deb6de742c496af9 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sat, 17 Feb 2024 19:04:41 +1100 Subject: [PATCH 4/6] Refactor for payment processing --- app/Services/Invoice/AutoBillInvoice.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Services/Invoice/AutoBillInvoice.php b/app/Services/Invoice/AutoBillInvoice.php index dd0da025ab99..3c864a6a88f3 100644 --- a/app/Services/Invoice/AutoBillInvoice.php +++ b/app/Services/Invoice/AutoBillInvoice.php @@ -288,24 +288,24 @@ class AutoBillInvoice extends AbstractService if ($this->is_partial_amount) { //more than needed if ($payment_balance > $this->invoice->partial) { - $payload = ['invoices' => [['invoice_id' => $this->invoice->id,'amount' => $this->invoice->partial]]]; + $payload = ['client_id' => $this->invoice->client_id, 'invoices' => [['invoice_id' => $this->invoice->id,'amount' => $this->invoice->partial]]]; $payment_repo->save($payload, $payment); break; } else { - $payload = ['invoices' => [['invoice_id' => $this->invoice->id,'amount' => $payment_balance]]]; + $payload = ['client_id' => $this->invoice->client_id, 'invoices' => [['invoice_id' => $this->invoice->id,'amount' => $payment_balance]]]; $payment_repo->save($payload, $payment); } } else { //more than needed if ($payment_balance > $this->invoice->balance) { - $payload = ['invoices' => [['invoice_id' => $this->invoice->id,'amount' => $this->invoice->balance]]]; + $payload = ['client_id' => $this->invoice->client_id, 'invoices' => [['invoice_id' => $this->invoice->id,'amount' => $this->invoice->balance]]]; $payment_repo->save($payload, $payment); break; } else { - $payload = ['invoices' => [['invoice_id' => $this->invoice->id,'amount' => $payment_balance]]]; + $payload = ['client_id' => $this->invoice->client_id, 'invoices' => [['invoice_id' => $this->invoice->id,'amount' => $payment_balance]]]; $payment_repo->save($payload, $payment); } From 6974841921c78b716ba7ce059974fe1323b16e80 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sat, 17 Feb 2024 19:08:20 +1100 Subject: [PATCH 5/6] Refactor for payment processing --- app/Services/Invoice/AutoBillInvoice.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/Services/Invoice/AutoBillInvoice.php b/app/Services/Invoice/AutoBillInvoice.php index 3c864a6a88f3..22eacad77639 100644 --- a/app/Services/Invoice/AutoBillInvoice.php +++ b/app/Services/Invoice/AutoBillInvoice.php @@ -279,7 +279,6 @@ class AutoBillInvoice extends AbstractService $this->is_partial_amount = true; } - $payment_repo = new PaymentRepository(new CreditRepository()); foreach ($unapplied_payments as $key => $payment) { @@ -296,7 +295,7 @@ class AutoBillInvoice extends AbstractService $payment_repo->save($payload, $payment); } } else { - //more than needed + //more than needed if ($payment_balance > $this->invoice->balance) { $payload = ['client_id' => $this->invoice->client_id, 'invoices' => [['invoice_id' => $this->invoice->id,'amount' => $this->invoice->balance]]]; @@ -312,6 +311,10 @@ class AutoBillInvoice extends AbstractService } $this->invoice = $this->invoice->fresh(); + + if((int)$this->invoice->balance == 0) { + return $this; + } } return $this; From 730e0a17acdfe3ab1046dcc26aa2900147064bb2 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 18 Feb 2024 15:06:20 +1100 Subject: [PATCH 6/6] Tests for using Unapplied Payments --- app/Services/Invoice/AutoBillInvoice.php | 16 +- .../Payments/AutoUnappliedPaymentTest.php | 180 ++++++++++++++++++ 2 files changed, 184 insertions(+), 12 deletions(-) create mode 100644 tests/Feature/Payments/AutoUnappliedPaymentTest.php diff --git a/app/Services/Invoice/AutoBillInvoice.php b/app/Services/Invoice/AutoBillInvoice.php index 22eacad77639..549efee15b1d 100644 --- a/app/Services/Invoice/AutoBillInvoice.php +++ b/app/Services/Invoice/AutoBillInvoice.php @@ -68,7 +68,9 @@ class AutoBillInvoice extends AbstractService $this->applyCreditPayment(); } + nlog($this->client->getSetting('use_unapplied_payment')); if($this->client->getSetting('use_unapplied_payment') != 'off') { + nlog("meeeeeeerp"); $this->applyUnappliedPayment(); } @@ -182,9 +184,6 @@ class AutoBillInvoice extends AbstractService $payment->amount = 0; $payment->applied = 0; - - // $payment->amount = $amount; - // $payment->applied = $amount; $payment->client_id = $this->invoice->client_id; $payment->currency_id = $this->invoice->client->getSetting('currency_id'); $payment->date = now()->addSeconds($this->invoice->company->utc_offset())->format('Y-m-d'); @@ -259,7 +258,8 @@ class AutoBillInvoice extends AbstractService */ private function applyUnappliedPayment(): self { - $unapplied_payments = Payment::query()->where('client_id', $this->client->id) + $unapplied_payments = Payment::query() + ->where('client_id', $this->client->id) ->where('status_id', Payment::STATUS_COMPLETED) ->where('is_deleted', false) ->where('amount', '>', 'applied') @@ -408,14 +408,6 @@ class AutoBillInvoice extends AbstractService })->orderBy('is_default', 'DESC') ->get(); - // $gateway_tokens = $this->client - // ->gateway_tokens() - // ->whereHas('gateway', function ($query) { - // $query->where('is_deleted', 0) - // ->where('deleted_at', null); - // })->orderBy('is_default', 'DESC') - // ->get(); - $filtered_gateways = $gateway_tokens->filter(function ($gateway_token) use ($amount) { $company_gateway = $gateway_token->gateway; diff --git a/tests/Feature/Payments/AutoUnappliedPaymentTest.php b/tests/Feature/Payments/AutoUnappliedPaymentTest.php new file mode 100644 index 000000000000..96e3a967a22f --- /dev/null +++ b/tests/Feature/Payments/AutoUnappliedPaymentTest.php @@ -0,0 +1,180 @@ +faker = \Faker\Factory::create(); + + Model::reguard(); + + $this->makeTestData(); + // $this->withoutExceptionHandling(); + + $this->withoutMiddleware( + ThrottleRequests::class + ); + } + + public function testUnappliedPaymentsAreEnabled() + { + + $settings = ClientSettings::defaults(); + $settings->use_unapplied_payment = 'always'; + + $client = Client::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'settings' => $settings, + ]); + + $this->assertEquals('always', $client->settings->use_unapplied_payment); + + $invoice = Invoice::factory()->for($client)->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'auto_bill_enabled' => true, + 'client_id' => $client->id, + ]); + + $invoice = $invoice->calc()->getInvoice(); + + $payment = Payment::factory()->for($client)->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'client_id' => $client->id, + 'amount' => 100, + 'applied' => 0, + 'refunded' => 0, + 'status_id' => Payment::STATUS_COMPLETED, + 'is_deleted' => 0, + ]); + + $invoice->service()->markSent()->save(); + + $this->assertGreaterThan(0, $invoice->balance); + + nlog($invoice->balance); + + try{ + $invoice->service()->autoBill()->save(); + } + catch(\Exception $e){ + + } + + $invoice = $invoice->fresh(); + $payment = $payment->fresh(); + + nlog($invoice->toArray()); + nlog($payment->toArray()); + + $this->assertEquals($payment->applied, $invoice->paid_to_date); + $this->assertGreaterThan(2, $invoice->status_id); + $this->assertGreaterThan(0, $payment->applied); + + // $this->assertEquals(Invoice::STATUS_PAID, $invoice->status_id); + // $this->assertEquals(0, $invoice->balance); + + } + + + public function testUnappliedPaymentsAreDisabled() + { + + $settings = ClientSettings::defaults(); + $settings->use_unapplied_payment = 'off'; + + $client = Client::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'settings' => $settings, + ]); + + $this->assertEquals('off', $client->settings->use_unapplied_payment); + + $invoice = Invoice::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'client_id' => $client->id, + 'auto_bill_enabled' => true, + 'status_id' => 2 + ]); + $invoice = $invoice->calc()->getInvoice(); + $invoice_balance = $invoice->balance; + + $payment = Payment::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'client_id' => $client->id, + 'amount' => 100, + 'applied' => 0, + 'refunded' => 0, + 'status_id' => Payment::STATUS_COMPLETED + ]); + + $invoice->service()->markSent()->save(); + + $this->assertGreaterThan(0, $invoice->balance); + + try { + $invoice->service()->autoBill()->save(); + } + catch(\Exception $e) { + + } + + $invoice = $invoice->fresh(); + $payment = $payment->fresh(); + + $this->assertEquals($invoice_balance, $invoice->balance); + $this->assertEquals(0, $payment->applied); + $this->assertEquals(2, $invoice->status_id); + $this->assertEquals(0, $invoice->paid_to_date); + $this->assertEquals($invoice->amount, $invoice->balance); + + // $this->assertEquals($payment->applied, $invoice->paid_to_date); + // $this->assertEquals(2, $invoice->status_id); + + + } + +} \ No newline at end of file