From 2fd3229efd8ebfc09a8627aef327128e374faad5 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 8 Apr 2020 20:48:31 +1000 Subject: [PATCH] Implementation of Invoice Reversal (#3602) * Version bump 0.0.2 * code cleanup * Working on reversing an invoice * Working on reversing an invoice * Working on refunding invoice * Reversing invoices * Test for invoice reversals * Invoice Reversal --- VERSION.txt | 2 +- app/Http/Controllers/InvoiceController.php | 5 + .../Requests/Invoice/ActionInvoiceRequest.php | 31 +--- .../Requests/Payment/StorePaymentRequest.php | 6 +- .../Credit/ValidCreditsRules.php | 92 +++++++++ .../ValidCreditsPresentRule.php | 8 +- app/Jobs/Credit/ApplyCreditPayment.php | 2 +- app/Models/Client.php | 2 - app/Models/Credit.php | 5 + app/Models/Invoice.php | 23 +-- app/Models/Paymentable.php | 5 + app/Repositories/InvoiceRepository.php | 14 +- app/Repositories/PaymentRepository.php | 7 +- app/Services/Invoice/HandleDeletion.php | 54 ------ app/Services/Invoice/HandleReversal.php | 110 +++++++++++ app/Services/Invoice/InvoiceService.php | 6 +- app/Services/Invoice/MarkSent.php | 2 - app/Services/Ledger/LedgerService.php | 3 +- app/Utils/Traits/Invoice/ActionsInvoice.php | 14 +- tests/Feature/PaymentTest.php | 121 ++++++++++++ tests/Feature/RefundTest.php | 9 +- tests/Feature/ReverseInvoiceTest.php | 175 ++++++++++++++++++ tests/Unit/InvoiceActionsTest.php | 24 +-- 23 files changed, 586 insertions(+), 134 deletions(-) create mode 100644 app/Http/ValidationRules/Credit/ValidCreditsRules.php delete mode 100644 app/Services/Invoice/HandleDeletion.php create mode 100644 app/Services/Invoice/HandleReversal.php create mode 100644 tests/Feature/ReverseInvoiceTest.php diff --git a/VERSION.txt b/VERSION.txt index 8a9ecc2ea99d..7bcd0e3612da 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -0.0.1 \ No newline at end of file +0.0.2 \ No newline at end of file diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index a38adaf607f1..558e3525a07f 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -679,6 +679,11 @@ class InvoiceController extends BaseController case 'cancel': break; case 'reverse': + $invoice = $invoice->service()->handleReversal()->save(); + + if(!$bulk){ + $this->itemResponse($invoice); + } break; case 'email': diff --git a/app/Http/Requests/Invoice/ActionInvoiceRequest.php b/app/Http/Requests/Invoice/ActionInvoiceRequest.php index 938cfafcaa2d..e47829777536 100644 --- a/app/Http/Requests/Invoice/ActionInvoiceRequest.php +++ b/app/Http/Requests/Invoice/ActionInvoiceRequest.php @@ -50,15 +50,15 @@ class ActionInvoiceRequest extends Request if(!array_key_exists('action', $input) { $this->error_msg = 'Action is a required field'; } - elseif(!$this->invoiceDeletable()){ + elseif(!$this->invoiceDeletable($this->invoice)){ unset($input['action']); $this->error_msg = 'This invoice cannot be deleted'; } - elseif(!$this->invoiceCancellable()) { + elseif(!$this->invoiceCancellable($this->invoice)) { unset($input['action']); $this->error_msg = 'This invoice cannot be cancelled'; } - else if(!$this->invoiceReversable()) { + else if(!$this->invoiceReversable($this->invoice)) { unset($input['action']); $this->error_msg = 'This invoice cannot be reversed'; } @@ -74,32 +74,7 @@ class ActionInvoiceRequest extends Request } - private function invoiceDeletable() - { - if($this->invoice->status_id <= 2 && $this->invoice->is_deleted == false && $this->invoice->deleted_at == NULL) - return true; - - return false; - } - - private function invoiceCancellable() - { - - if($this->invoice->status_id == 3 && $this->invoice->is_deleted == false && $this->invoice->deleted_at == NULL) - return true; - - return false; - } - - private function invoiceReversable() - { - - if(($this->invoice->status_id == 3 || $this->invoice->status_id == 4) && $this->invoice->is_deleted == false && $this->invoice->deleted_at == NULL) - return true; - - return false; - } } diff --git a/app/Http/Requests/Payment/StorePaymentRequest.php b/app/Http/Requests/Payment/StorePaymentRequest.php index 2458e5aceb5f..e553cfba30d3 100644 --- a/app/Http/Requests/Payment/StorePaymentRequest.php +++ b/app/Http/Requests/Payment/StorePaymentRequest.php @@ -12,6 +12,7 @@ namespace App\Http\Requests\Payment; use App\Http\Requests\Request; +use App\Http\ValidationRules\Credit\ValidCreditsRules; use App\Http\ValidationRules\PaymentAmountsBalanceRule; use App\Http\ValidationRules\Payment\ValidInvoicesRules; use App\Http\ValidationRules\ValidCreditsPresentRule; @@ -50,8 +51,6 @@ class StorePaymentRequest extends Request $input['invoices'][$key]['invoice_id'] = $this->decodePrimaryKey($value['invoice_id']); $invoices_total += $value['amount']; } - - //if(!isset($input['amount']) || ) } if (isset($input['invoices']) && is_array($input['invoices']) === false) { @@ -61,7 +60,7 @@ class StorePaymentRequest extends Request if (isset($input['credits']) && is_array($input['credits']) !== false) { foreach ($input['credits'] as $key => $value) { if (array_key_exists('credit_id', $input['credits'][$key])) { - $input['credits'][$key]['credit_id'] = $this->decodePrimaryKey($value['credit_id']); + $input['credits'][$key]['credit_id'] = $value['credit_id']; $credits_total += $value['amount']; } } @@ -91,6 +90,7 @@ class StorePaymentRequest extends Request 'invoices.*.invoice_id' => new ValidInvoicesRules($this->all()), 'invoices.*.amount' => 'required', 'credits.*.credit_id' => 'required|exists:credits,id', + 'credits.*.credit_id' => new ValidCreditsRules($this->all()), 'credits.*.amount' => 'required', 'invoices' => new ValidPayableInvoicesRule(), 'number' => 'nullable', diff --git a/app/Http/ValidationRules/Credit/ValidCreditsRules.php b/app/Http/ValidationRules/Credit/ValidCreditsRules.php new file mode 100644 index 000000000000..1cdaf11eca44 --- /dev/null +++ b/app/Http/ValidationRules/Credit/ValidCreditsRules.php @@ -0,0 +1,92 @@ +input = $input; + } + + public function passes($attribute, $value) + { + return $this->checkCreditsAreHomogenous(); + } + + private function checkCreditsAreHomogenous() + { + if (!array_key_exists('client_id', $this->input)) { + $this->error_msg = "Client id is required"; + return false; + } + + $unique_array = []; + + foreach ($this->input['credits'] as $credit) { + $unique_array[] = $credit['credit_id']; + + $cred = Credit::find($this->decodePrimaryKey($credit['credit_id'])); + + if (!$cred) { + $this->error_msg = "Credit not found "; + return false; + } + + if ($cred->client_id != $this->input['client_id']) { + $this->error_msg = "Selected invoices are not from a single client"; + return false; + } + } + + if (!(array_unique($unique_array) == $unique_array)) { + $this->error_msg = "Duplicate credits submitted."; + return false; + } + + + return true; + } + + + /** + * @return string + */ + public function message() + { + return $this->error_msg; + } +} diff --git a/app/Http/ValidationRules/ValidCreditsPresentRule.php b/app/Http/ValidationRules/ValidCreditsPresentRule.php index 684479481fc8..beff5100126f 100644 --- a/app/Http/ValidationRules/ValidCreditsPresentRule.php +++ b/app/Http/ValidationRules/ValidCreditsPresentRule.php @@ -43,17 +43,17 @@ class ValidCreditsPresentRule implements Rule return 'Insufficient balance on credit.'; } - - private function validCreditsPresent() :bool { //todo need to ensure the clients credits are here not random ones! if (request()->input('credits') && is_array(request()->input('credits'))) { + foreach (request()->input('credits') as $credit) { + $cred = Credit::find($this->decodePrimaryKey($credit['credit_id'])); - - if ($cred->balance == 0) { + + if (!$cred || $cred->balance == 0) { return false; } } diff --git a/app/Jobs/Credit/ApplyCreditPayment.php b/app/Jobs/Credit/ApplyCreditPayment.php index 4babaa313c53..74d89f001337 100644 --- a/app/Jobs/Credit/ApplyCreditPayment.php +++ b/app/Jobs/Credit/ApplyCreditPayment.php @@ -71,7 +71,7 @@ class ApplyCreditPayment implements ShouldQueue $this->credit->setStatus(Credit::STATUS_APPLIED); $this->credit->updateBalance($this->amount*-1); } elseif ($this->amount < $credit_balance) { //compare number appropriately - $this->credit->setStatus(Credit::PARTIAL); + $this->credit->setStatus(Credit::STATUS_PARTIAL); $this->credit->updateBalance($this->amount*-1); } diff --git a/app/Models/Client.php b/app/Models/Client.php index fc077fb433c4..12e02734bffb 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -332,8 +332,6 @@ class Client extends BaseModel implements HasLocalePreference { $company_gateways = $this->getSetting('company_gateway_ids'); - info($company_gateways); - info($this->company->id); if (strlen($company_gateways)>=1) { $gateways = $this->company->company_gateways->whereIn('id', $payment_gateways); } else { diff --git a/app/Models/Credit.php b/app/Models/Credit.php index 3d65b1e454cf..9ba26acd7337 100644 --- a/app/Models/Credit.php +++ b/app/Models/Credit.php @@ -16,6 +16,7 @@ use App\Helpers\Invoice\InvoiceSumInclusive; use App\Jobs\Credit\CreateCreditPdf; use App\Models\Filterable; use App\Services\Credit\CreditService; +use App\Services\Ledger\LedgerService; use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesInvoiceValues; @@ -143,6 +144,10 @@ class Credit extends BaseModel return $this->morphMany(CompanyLedger::class, 'company_ledgerable'); } + public function ledger() + { + return new LedgerService($this); + } /** * The invoice/s which the credit has * been applied to. diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 0d772959d42a..e0df9f8fc964 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -27,6 +27,7 @@ use App\Services\Invoice\InvoiceService; use App\Services\Ledger\LedgerService; use App\Utils\Number; use App\Utils\Traits\InvoiceEmailBuilder; +use App\Utils\Traits\Invoice\ActionsInvoice; use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesInvoiceValues; use App\Utils\Traits\MakesReminders; @@ -48,6 +49,7 @@ class Invoice extends BaseModel use MakesInvoiceValues; use InvoiceEmailBuilder; use MakesReminders; + use ActionsInvoice; protected $presenter = 'App\Models\Presenters\InvoicePresenter'; @@ -123,10 +125,10 @@ class Invoice extends BaseModel const STATUS_PARTIAL = 3; const STATUS_PAID = 4; const STATUS_CANCELLED = 5; + const STATUS_REVERSED = 6; - const STATUS_OVERDUE = -1; - const STATUS_UNPAID = -2; - const STATUS_REVERSED = -3; + const STATUS_OVERDUE = -1; //status < 4 || < 3 && !is_deleted && !trashed() && due_date < now() + const STATUS_UNPAID = -2; //status < 4 || < 3 && !is_deleted && !trashed() public function getDateAttribute($value) { @@ -193,14 +195,13 @@ class Invoice extends BaseModel return $this->morphMany(CompanyLedger::class, 'company_ledgerable'); } - public function credits() - { - return $this->belongsToMany(Credit::class)->using(Paymentable::class)->withPivot( - 'amount', - 'refunded' - )->withTimestamps(); - ; - } + // public function credits() + // { + // return $this->belongsToMany(Credit::class)->using(Paymentable::class)->withPivot( + // 'amount', + // 'refunded' + // )->withTimestamps(); + // } /** * Service entry points diff --git a/app/Models/Paymentable.php b/app/Models/Paymentable.php index 6fa3c7a44a89..767bbbf1257f 100644 --- a/app/Models/Paymentable.php +++ b/app/Models/Paymentable.php @@ -35,4 +35,9 @@ class Paymentable extends Pivot { return $this->morphTo(); } + + public function payment() + { + return $this->belongsTo(Payment::class); + } } diff --git a/app/Repositories/InvoiceRepository.php b/app/Repositories/InvoiceRepository.php index 173eaeaf8229..e03a56dd54d2 100644 --- a/app/Repositories/InvoiceRepository.php +++ b/app/Repositories/InvoiceRepository.php @@ -68,7 +68,17 @@ class InvoiceRepository extends BaseRepository return InvoiceInvitation::whereRaw("BINARY `key`= ?", [$key])->first(); } - public function delete($invoice) + /** + * Method is not protected, assumes that + * other protections have been implemented prior + * to hitting this method. + * + * ie. invoice can be deleted from a business logic perspective. + * + * @param Invoice $invoice + * @return Invoice $invoice + */ + public function delete($invoice) { if ($invoice->is_deleted) { return; @@ -82,6 +92,8 @@ class InvoiceRepository extends BaseRepository if (class_exists($className)) { event(new InvoiceWasDeleted($invoice)); } + + return $invoice; } public function reverse() diff --git a/app/Repositories/PaymentRepository.php b/app/Repositories/PaymentRepository.php index e98d96c50cfb..4d653e6c3ad3 100644 --- a/app/Repositories/PaymentRepository.php +++ b/app/Repositories/PaymentRepository.php @@ -19,6 +19,7 @@ use App\Models\Credit; use App\Models\Invoice; use App\Models\Payment; use App\Repositories\CreditRepository; +use App\Utils\Traits\MakesHash; use Illuminate\Http\Request; /** @@ -26,6 +27,8 @@ use Illuminate\Http\Request; */ class PaymentRepository extends BaseRepository { + use MakesHash; + protected $credit_repo; public function __construct(CreditRepository $credit_repo) @@ -105,12 +108,12 @@ class PaymentRepository extends BaseRepository if (array_key_exists('credits', $data) && is_array($data['credits'])) { $credit_totals = array_sum(array_column($data['credits'], 'amount')); - $credits = Credit::whereIn('id', array_column($data['credits'], 'credit_id'))->get(); + $credits = Credit::whereIn('id', $this->transformKeys(array_column($data['credits'], 'credit_id')))->get(); $payment->credits()->saveMany($credits); foreach ($data['credits'] as $paid_credit) { - $credit = Credit::whereId($paid_credit['credit_id'])->first(); + $credit = Credit::find($this->decodePrimaryKey($paid_credit['credit_id'])); if ($credit) { ApplyCreditPayment::dispatchNow($credit, $payment, $paid_credit['amount'], $credit->company); diff --git a/app/Services/Invoice/HandleDeletion.php b/app/Services/Invoice/HandleDeletion.php deleted file mode 100644 index 30cb2aaa4bd0..000000000000 --- a/app/Services/Invoice/HandleDeletion.php +++ /dev/null @@ -1,54 +0,0 @@ -invoice = $invoice; - } - - public function run() - { - $balance_remaining = $this->invoice->balance; - $total_paid = $this->invoice->amount - $this->invoice->balance; - - //change invoice status - - //set invoice balance to 0 - - //decrease client balance by $total_paid - - //remove paymentables from payment - - //decreate client paid_to_date by $total_paid - - //generate credit for the $total paid - - } -} diff --git a/app/Services/Invoice/HandleReversal.php b/app/Services/Invoice/HandleReversal.php new file mode 100644 index 000000000000..c6e736972d4b --- /dev/null +++ b/app/Services/Invoice/HandleReversal.php @@ -0,0 +1,110 @@ +invoice = $invoice; + } + + public function run() + { + /* Check again!! */ + if(!$this->invoice->invoiceReversable($this->invoice)) + return $this->invoice; + + $balance_remaining = $this->invoice->balance; + + $total_paid = $this->invoice->amount - $this->invoice->balance; + + /*Adjust payment applied and the paymentables to the correct amount */ + + $paymentables = Paymentable::wherePaymentableType(Invoice::class) + ->wherePaymentableId($this->invoice->id) + ->get(); + + $paymentables->each(function ($paymentable) use($total_paid){ + + $reversable_amount = $paymentable->amount - $paymentable->refunded; + + $total_paid -= $reversable_amount; + + $paymentable->amount = $paymentable->refunded; + $paymentable->save(); + + }); + + /* Generate a credit for the $total_paid amount */ + $credit = CreditFactory::create($this->invoice->company_id, $this->invoice->user_id); + $credit->client_id = $this->invoice->client_id; + + $item = InvoiceItemFactory::create(); + $item->quantity = 1; + $item->cost = (float)$total_paid; + $item->notes = "Credit for reversal of ".$this->invoice->number; + + $line_items[] = $item; + + $credit->line_items = $line_items; + + $credit->save(); + + $credit_calc = new InvoiceSum($credit); + $credit_calc->build(); + + $credit = $credit_calc->getCredit(); + + $credit->service()->markSent()->save(); + + /* Set invoice balance to 0 */ + $this->invoice->ledger()->updateInvoiceBalance($balance_remaining, $item->notes)->save(); + + $this->invoice->balance= 0; + + /* Set invoice status to reversed... somehow*/ + $this->invoice->service()->setStatus(Invoice::STATUS_REVERSED)->save(); + + /* Reduce client.paid_to_date by $total_paid amount */ + /* Reduce the client balance by $balance_remaining */ + + $this->invoice->client->service() + ->updateBalance($balance_remaining*-1) + ->updatePaidToDate($total_paid*-1) + ->save(); + + return $this->invoice; + //create a ledger row for this with the resulting Credit ( also include an explanation in the notes section ) + + } +} + +// The client paid to date amount is reduced by the calculated amount of (invoice balance - invoice amount). diff --git a/app/Services/Invoice/InvoiceService.php b/app/Services/Invoice/InvoiceService.php index f960a4f3d340..4730ae379000 100644 --- a/app/Services/Invoice/InvoiceService.php +++ b/app/Services/Invoice/InvoiceService.php @@ -18,7 +18,7 @@ use App\Services\Invoice\ApplyNumber; use App\Services\Invoice\ApplyPayment; use App\Services\Invoice\CreateInvitations; use App\Services\Invoice\GetInvoicePdf; -use App\Services\Invoice\HandleDeletion; +use App\Services\Invoice\HandleReversal; use App\Services\Invoice\MarkInvoicePaid; use App\Services\Invoice\MarkSent; use App\Services\Invoice\UpdateBalance; @@ -116,9 +116,9 @@ class InvoiceService return $send_email->run(); } - public function handleDeletion() + public function handleReversal() { - $this->invoice = (new HandleDeletion($this->invoice))->run(); + $this->invoice = (new HandleReversal($this->invoice))->run(); return $this; } diff --git a/app/Services/Invoice/MarkSent.php b/app/Services/Invoice/MarkSent.php index d63025194bbb..fc1345b83b61 100644 --- a/app/Services/Invoice/MarkSent.php +++ b/app/Services/Invoice/MarkSent.php @@ -47,8 +47,6 @@ class MarkSent extends AbstractService $this->invoice->ledger()->updateInvoiceBalance($this->invoice->balance); - //UpdateCompanyLedgerWithInvoice::dispatchNow($this->invoice, $this->invoice->balance, $this->invoice->company); - return $this->invoice; } } diff --git a/app/Services/Ledger/LedgerService.php b/app/Services/Ledger/LedgerService.php index f3202a547de9..e81892de3cd6 100644 --- a/app/Services/Ledger/LedgerService.php +++ b/app/Services/Ledger/LedgerService.php @@ -23,7 +23,7 @@ class LedgerService $this->entity = $entity; } - public function updateInvoiceBalance($adjustment) + public function updateInvoiceBalance($adjustment, $notes = '') { $balance = 0; @@ -36,6 +36,7 @@ class LedgerService $company_ledger = CompanyLedgerFactory::create($this->entity->company_id, $this->entity->user_id); $company_ledger->client_id = $this->entity->client_id; $company_ledger->adjustment = $adjustment; + $company_ledger->notes = $notes; $company_ledger->balance = $balance + $adjustment; $company_ledger->save(); diff --git a/app/Utils/Traits/Invoice/ActionsInvoice.php b/app/Utils/Traits/Invoice/ActionsInvoice.php index 51cd1a0da757..e9d3efaec6a9 100644 --- a/app/Utils/Traits/Invoice/ActionsInvoice.php +++ b/app/Utils/Traits/Invoice/ActionsInvoice.php @@ -11,31 +11,33 @@ namespace App\Utils\Traits\Invoice; +use App\Models\Invoice; + trait ActionsInvoice { - public function invoiceDeletable() :bool + public function invoiceDeletable($invoice) :bool { - if($this->invoice->status_id <= 2 && $this->invoice->is_deleted == false && $this->invoice->deleted_at == NULL) + if($invoice->status_id <= Invoice::STATUS_SENT && $invoice->is_deleted == false && $invoice->deleted_at == NULL) return true; return false; } - public function invoiceCancellable() :bool + public function invoiceCancellable($invoice) :bool { - if($this->invoice->status_id == 3 && $this->invoice->is_deleted == false && $this->invoice->deleted_at == NULL) + if($invoice->status_id == Invoice::STATUS_PARTIAL && $invoice->is_deleted == false && $invoice->deleted_at == NULL) return true; return false; } - public function invoiceReversable() :bool + public function invoiceReversable($invoice) :bool { - if(($this->invoice->status_id == 3 || $this->invoice->status_id == 4) && $this->invoice->is_deleted == false && $this->invoice->deleted_at == NULL) + if(($invoice->status_id == Invoice::STATUS_SENT || $invoice->status_id == Invoice::STATUS_PARTIAL || $invoice->status_id == Invoice::STATUS_PAID) && $invoice->is_deleted == false && $invoice->deleted_at == NULL) return true; return false; diff --git a/tests/Feature/PaymentTest.php b/tests/Feature/PaymentTest.php index 734dae9da63b..c44e7832b5ad 100644 --- a/tests/Feature/PaymentTest.php +++ b/tests/Feature/PaymentTest.php @@ -5,12 +5,14 @@ namespace Tests\Feature; use App\DataMapper\ClientSettings; use App\DataMapper\CompanySettings; use App\Factory\ClientFactory; +use App\Factory\CreditFactory; use App\Factory\InvoiceFactory; use App\Factory\PaymentFactory; use App\Helpers\Invoice\InvoiceSum; use App\Models\Account; use App\Models\Activity; use App\Models\Client; +use App\Models\Credit; use App\Models\Invoice; use App\Models\Payment; use App\Utils\Traits\MakesHash; @@ -18,6 +20,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\WithFaker; +use Illuminate\Foundation\Testing\WithoutEvents; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Session; use Illuminate\Validation\ValidationException; @@ -34,6 +37,7 @@ class PaymentTest extends TestCase use MakesHash; use DatabaseTransactions; use MockAccountData; + use WithoutEvents; public function setUp() :void { @@ -413,6 +417,22 @@ class PaymentTest extends TestCase $client = ClientFactory::create($this->company->id, $this->user->id); $client->save(); + factory(\App\Models\ClientContact::class, 1)->create([ + 'user_id' => $this->user->id, + 'client_id' => $client->id, + 'company_id' => $this->company->id, + 'is_primary' => 1, + 'send_email' => true, + ]); + + factory(\App\Models\ClientContact::class, 1)->create([ + 'user_id' => $this->user->id, + 'client_id' => $client->id, + 'company_id' => $this->company->id, + 'send_email' => true + ]); + + $this->invoice = InvoiceFactory::create($this->company->id, $this->user->id);//stub the company and user_id $this->invoice->client_id = $client->id; @@ -471,6 +491,23 @@ class PaymentTest extends TestCase $client = ClientFactory::create($this->company->id, $this->user->id); $client->save(); + $contact = factory(\App\Models\ClientContact::class, 1)->create([ + 'user_id' => $this->user->id, + 'client_id' => $this->client->id, + 'company_id' => $this->company->id, + 'is_primary' => 1, + 'send_email' => true, + ]); + + factory(\App\Models\ClientContact::class, 1)->create([ + 'user_id' => $this->user->id, + 'client_id' => $this->client->id, + 'company_id' => $this->company->id, + 'send_email' => true + ]); + + $client->setRelation('contact', $contact); + $this->invoice = InvoiceFactory::create($this->company->id, $this->user->id);//stub the company and user_id $this->invoice->client_id = $client->id; @@ -487,6 +524,7 @@ class PaymentTest extends TestCase $this->invoice->save(); $this->invoice->service()->markSent()->save(); + $this->invoice->setRelation('client', $client); $data = [ 'amount' => 1.0, @@ -1130,4 +1168,87 @@ class PaymentTest extends TestCase $this->assertNull($response); } + + + public function testStorePaymentWithCredits() + { + $client = ClientFactory::create($this->company->id, $this->user->id); + $client->save(); + + $this->invoice = InvoiceFactory::create($this->company->id, $this->user->id);//stub the company and user_id + $this->invoice->client_id = $client->id; + $this->invoice->status_id = Invoice::STATUS_SENT; + + $this->invoice->line_items = $this->buildLineItems(); + $this->invoice->uses_inclusive_taxes = false; + + $this->invoice->save(); + + $this->invoice_calc = new InvoiceSum($this->invoice); + $this->invoice_calc->build(); + + $this->invoice = $this->invoice_calc->getInvoice(); + $this->invoice->save(); + + $credit = CreditFactory::create($this->company->id, $this->user->id); + $credit->client_id = $client->id; + $credit->status_id = Credit::STATUS_SENT; + + $credit->line_items = $this->buildLineItems(); + $credit->uses_inclusive_taxes = false; + + $credit->save(); + + $credit_calc = new InvoiceSum($credit); + $credit_calc->build(); + + $credit = $this->credit_calc->getCredit(); + $credit->save(); //$10 credit + + + $data = [ + 'amount' => $this->invoice->amount, + 'client_id' => $client->hashed_id, + 'invoices' => [ + [ + 'invoice_id' => $this->invoice->hashed_id, + 'amount' => 5 + ], + ], + 'credits' => [ + [ + 'credit_id' => $credit->id, + 'amount' => 5 + ], + ], + 'date' => '2020/12/12', + + ]; + + $response = null; + + try { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/payments?include=invoices', $data); + } catch (ValidationException $e) { + $message = json_decode($e->validator->getMessageBag(), 1); + $this->assertNotNull($message); + } + + if ($response) { + $arr = $response->json(); + $response->assertStatus(200); + + $payment_id = $arr['data']['id']; + + $payment = Payment::find($this->decodePrimaryKey($payment_id))->first(); + + $this->assertNotNull($payment); + $this->assertNotNull($payment->invoices()); + $this->assertEquals(1, $payment->invoices()->count()); + } + } + } diff --git a/tests/Feature/RefundTest.php b/tests/Feature/RefundTest.php index 38ff9caa2a72..898ac90a82ab 100644 --- a/tests/Feature/RefundTest.php +++ b/tests/Feature/RefundTest.php @@ -505,9 +505,9 @@ class RefundTest extends TestCase $this->invoice->line_items = $this->buildLineItems(); $this->invoice->uses_inclusive_taxes = false; + $this->invoice_client_id = $client->id; $this->invoice->save(); - $this->invoice_calc = new InvoiceSum($this->invoice); $this->invoice_calc->build(); @@ -515,7 +515,8 @@ class RefundTest extends TestCase $this->invoice->save(); $this->credit = CreditFactory::create($this->company->id, $this->user->id); - $this->credit->client_id = $this->client->id; + $this->credit->client_id = $client->id; + $this->credit->status_id=2; $this->credit->line_items = $this->buildLineItems(); $this->credit->amount = 10; @@ -552,9 +553,11 @@ class RefundTest extends TestCase ])->post('/api/v1/payments', $data); } catch (ValidationException $e) { $message = json_decode($e->validator->getMessageBag(), 1); + \Log::error("this should not hit"); \Log::error($message); } - + + $arr = $response->json(); $response->assertStatus(200); diff --git a/tests/Feature/ReverseInvoiceTest.php b/tests/Feature/ReverseInvoiceTest.php new file mode 100644 index 000000000000..a5f4cc6b37e2 --- /dev/null +++ b/tests/Feature/ReverseInvoiceTest.php @@ -0,0 +1,175 @@ +withoutMiddleware( + ThrottleRequests::class + ); + + $this->faker = \Faker\Factory::create(); + + Model::reguard(); + + $this->makeTestData(); + + $this->withoutExceptionHandling(); + } + + public function testReverseInvoice() + { + + $amount = $this->invoice->amount; + $balance = $this->invoice->balance; + + $this->invoice->service()->markPaid()->save(); + + $first_payment = $this->invoice->payments->first(); + + $this->assertEquals((float)$first_payment->amount, (float)$this->invoice->amount); + $this->assertEquals((float)$first_payment->applied, (float)$this->invoice->amount); + + $this->assertTrue($this->invoice->invoiceReversable($this->invoice)); + + $balance_remaining = $this->invoice->balance; + $total_paid = $this->invoice->amount - $this->invoice->balance; + + /*Adjust payment applied and the paymentables to the correct amount */ + + $paymentables = Paymentable::wherePaymentableType(Invoice::class) + ->wherePaymentableId($this->invoice->id) + ->get(); + + $paymentables->each(function ($paymentable) use($total_paid){ + + $reversable_amount = $paymentable->amount - $paymentable->refunded; + + $total_paid -= $reversable_amount; + + $paymentable->amount = $paymentable->refunded; + $paymentable->save(); + + }); + + /* Generate a credit for the $total_paid amount */ + $credit = CreditFactory::create($this->invoice->company_id, $this->invoice->user_id); + $credit->client_id = $this->invoice->client_id; + + $item = InvoiceItemFactory::create(); + $item->quantity = 1; + $item->cost = (float)$total_paid; + $item->notes = "Credit for reversal of ".$this->invoice->number; + + $line_items[] = $item; + + $credit->line_items = $line_items; + + $credit->save(); + + $credit_calc = new InvoiceSum($credit); + $credit_calc->build(); + + $credit = $credit_calc->getCredit(); + + $credit->service() + ->setStatus(Credit::STATUS_SENT) + ->markSent()->save(); + + /* Set invoice balance to 0 */ + $this->invoice->ledger()->updateInvoiceBalance($balance_remaining*-1, $item->notes)->save(); + + /* Set invoice status to reversed... somehow*/ + $this->invoice->service()->setStatus(Invoice::STATUS_REVERSED)->save(); + + /* Reduce client.paid_to_date by $total_paid amount */ + $this->client->paid_to_date -= $total_paid; + + /* Reduce the client balance by $balance_remaining */ + $this->client->balance -= $balance_remaining; + + $this->client->save(); + + //create a ledger row for this with the resulting Credit ( also include an explanation in the notes section ) + } + + + public function testReversalViaAPI() + { + $this->assertEquals($this->client->balance, $this->invoice->balance); + + $client_paid_to_date = $this->client->paid_to_date; + $client_balance = $this->client->balance; + $invoice_balance = $this->invoice->balance; + + $this->assertEquals(Invoice::STATUS_SENT, $this->invoice->status_id); + + $this->invoice = $this->invoice->service()->markPaid()->save(); + + $this->assertEquals($this->client->balance, ($this->invoice->balance*-1)); + $this->assertEquals($this->client->paid_to_date, ($client_paid_to_date+$invoice_balance)); + $this->assertEquals(0, $this->invoice->balance); + $this->assertEquals(Invoice::STATUS_PAID, $this->invoice->status_id); + + $this->invoice = $this->invoice->service()->handleReversal()->save(); + + $this->assertEquals(Invoice::STATUS_REVERSED, $this->invoice->status_id); + $this->assertEquals(0, $this->invoice->balance); + $this->assertEquals($this->client->paid_to_date, ($client_paid_to_date)); + + + } + + public function testReversalNoPayment() + { + $this->assertEquals($this->client->balance, $this->invoice->balance); + + $client_paid_to_date = $this->client->paid_to_date; + $client_balance = $this->client->balance; + $invoice_balance = $this->invoice->balance; + + $this->assertEquals(Invoice::STATUS_SENT, $this->invoice->status_id); + + $this->invoice = $this->invoice->service()->handleReversal()->save(); + + $this->assertEquals(Invoice::STATUS_REVERSED, $this->invoice->status_id); + $this->assertEquals(0, $this->invoice->balance); + $this->assertEquals($this->client->paid_to_date, ($client_paid_to_date)); + $this->assertEquals($this->client->balance, ($client_balance-$invoice_balance)); + } +} + + diff --git a/tests/Unit/InvoiceActionsTest.php b/tests/Unit/InvoiceActionsTest.php index 89334b86aa2f..52739faa56cd 100644 --- a/tests/Unit/InvoiceActionsTest.php +++ b/tests/Unit/InvoiceActionsTest.php @@ -27,18 +27,18 @@ class InvoiceActionsTest extends TestCase public function testInvoiceIsDeletable() { - $this->assertTrue($this->invoiceDeletable()); - $this->assertFalse($this->invoiceReversable()); - $this->assertFalse($this->invoiceCancellable()); + $this->assertTrue($this->invoiceDeletable($this->invoice)); + $this->assertFalse($this->invoiceReversable($this->invoice)); + $this->assertFalse($this->invoiceCancellable($this->invoice)); } public function testInvoiceIsReversable() { $this->invoice->service()->markPaid()->save(); - $this->assertFalse($this->invoiceDeletable()); - $this->assertTrue($this->invoiceReversable()); - $this->assertFalse($this->invoiceCancellable()); + $this->assertFalse($this->invoiceDeletable($this->invoice)); + $this->assertTrue($this->invoiceReversable($this->invoice)); + $this->assertFalse($this->invoiceCancellable($this->invoice)); } public function testInvoiceIsCancellable() @@ -53,9 +53,9 @@ class InvoiceActionsTest extends TestCase $this->invoice->service()->applyPayment($payment, 5)->save(); - $this->assertFalse($this->invoiceDeletable()); - $this->assertTrue($this->invoiceReversable()); - $this->assertTrue($this->invoiceCancellable()); + $this->assertFalse($this->invoiceDeletable($this->invoice)); + $this->assertTrue($this->invoiceReversable($this->invoice)); + $this->assertTrue($this->invoiceCancellable($this->invoice)); } public function testInvoiceUnactionable() @@ -63,9 +63,9 @@ class InvoiceActionsTest extends TestCase $this->invoice->delete(); - $this->assertFalse($this->invoiceDeletable()); - $this->assertFalse($this->invoiceReversable()); - $this->assertFalse($this->invoiceCancellable()); + $this->assertFalse($this->invoiceDeletable($this->invoice)); + $this->assertFalse($this->invoiceReversable($this->invoice)); + $this->assertFalse($this->invoiceCancellable($this->invoice)); } }