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', diff --git a/app/Services/Invoice/AutoBillInvoice.php b/app/Services/Invoice/AutoBillInvoice.php index 73eac355eefe..549efee15b1d 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; @@ -31,7 +33,7 @@ class AutoBillInvoice extends AbstractService private Client $client; private array $used_credit = []; - + /*Specific variable for partial payments */ private bool $is_partial_amount = false; @@ -66,6 +68,12 @@ 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(); + } + //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; @@ -176,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'); @@ -217,8 +222,6 @@ class AutoBillInvoice extends AbstractService ->client ->service() ->updateBalanceAndPaidToDate($amount * -1, $amount) - // ->updateBalance($amount * -1) - // ->updatePaidToDate($amount) ->adjustCreditBalance($amount * -1) ->save(); @@ -243,6 +246,79 @@ 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; + } + + $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) { + $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 = ['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 = ['client_id' => $this->invoice->client_id, 'invoices' => [['invoice_id' => $this->invoice->id,'amount' => $this->invoice->balance]]]; + $payment_repo->save($payload, $payment); + + break; + } else { + + $payload = ['client_id' => $this->invoice->client_id, 'invoices' => [['invoice_id' => $this->invoice->id,'amount' => $payment_balance]]]; + $payment_repo->save($payload, $payment); + + } + } + + $this->invoice = $this->invoice->fresh(); + + if((int)$this->invoice->balance == 0) { + return $this; + } + } + + return $this; + } /** * Applies credits to a payment prior to push @@ -260,7 +336,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; @@ -332,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