mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-05-24 02:14:21 -04:00
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
This commit is contained in:
parent
9c2293427e
commit
2fd3229efd
@ -1 +1 @@
|
||||
0.0.1
|
||||
0.0.2
|
@ -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':
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -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',
|
||||
|
92
app/Http/ValidationRules/Credit/ValidCreditsRules.php
Normal file
92
app/Http/ValidationRules/Credit/ValidCreditsRules.php
Normal file
@ -0,0 +1,92 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com)
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://opensource.org/licenses/AAL
|
||||
*/
|
||||
|
||||
namespace App\Http\ValidationRules\Credit;
|
||||
|
||||
use App\Libraries\MultiDB;
|
||||
use App\Models\Credit;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Payment;
|
||||
use App\Models\User;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Illuminate\Contracts\Validation\Rule;
|
||||
|
||||
/**
|
||||
* Class ValidCreditsRules
|
||||
* @package App\Http\ValidationRules\Credit
|
||||
*/
|
||||
class ValidCreditsRules implements Rule
|
||||
{
|
||||
use MakesHash;
|
||||
|
||||
/**
|
||||
* @param string $attribute
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
private $error_msg;
|
||||
|
||||
private $input;
|
||||
|
||||
|
||||
public function __construct($input)
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -35,4 +35,9 @@ class Paymentable extends Pivot
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
public function payment()
|
||||
{
|
||||
return $this->belongsTo(Payment::class);
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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);
|
||||
|
@ -1,54 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com)
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://opensource.org/licenses/AAL
|
||||
*/
|
||||
|
||||
namespace App\Services\Invoice;
|
||||
|
||||
use App\Events\Payment\PaymentWasCreated;
|
||||
use App\Factory\PaymentFactory;
|
||||
use App\Models\Client;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Payment;
|
||||
use App\Services\AbstractService;
|
||||
use App\Services\Client\ClientService;
|
||||
use App\Services\Payment\PaymentService;
|
||||
use App\Utils\Traits\GeneratesCounter;
|
||||
|
||||
class HandleDeletion extends AbstractService
|
||||
{
|
||||
use GeneratesCounter;
|
||||
|
||||
|
||||
private $invoice;
|
||||
|
||||
public function __construct(Invoice $invoice)
|
||||
{
|
||||
$this->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
|
||||
|
||||
}
|
||||
}
|
110
app/Services/Invoice/HandleReversal.php
Normal file
110
app/Services/Invoice/HandleReversal.php
Normal file
@ -0,0 +1,110 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com)
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://opensource.org/licenses/AAL
|
||||
*/
|
||||
|
||||
namespace App\Services\Invoice;
|
||||
|
||||
use App\Events\Payment\PaymentWasCreated;
|
||||
use App\Factory\CreditFactory;
|
||||
use App\Factory\InvoiceItemFactory;
|
||||
use App\Factory\PaymentFactory;
|
||||
use App\Helpers\Invoice\InvoiceSum;
|
||||
use App\Models\Client;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Payment;
|
||||
use App\Models\Paymentable;
|
||||
use App\Services\AbstractService;
|
||||
use App\Services\Client\ClientService;
|
||||
use App\Services\Payment\PaymentService;
|
||||
use App\Utils\Traits\GeneratesCounter;
|
||||
|
||||
class HandleReversal extends AbstractService
|
||||
{
|
||||
use GeneratesCounter;
|
||||
|
||||
private $invoice;
|
||||
|
||||
public function __construct(Invoice $invoice)
|
||||
{
|
||||
$this->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).
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
175
tests/Feature/ReverseInvoiceTest.php
Normal file
175
tests/Feature/ReverseInvoiceTest.php
Normal file
@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Factory\CreditFactory;
|
||||
use App\Factory\InvoiceItemFactory;
|
||||
use App\Helpers\Invoice\InvoiceSum;
|
||||
use App\Listeners\Credit\CreateCreditInvitation;
|
||||
use App\Models\Client;
|
||||
use App\Models\Credit;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Payment;
|
||||
use App\Models\Paymentable;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Foundation\Testing\WithFaker;
|
||||
use Illuminate\Routing\Middleware\ThrottleRequests;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Tests\MockAccountData;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @covers App\Services\Invoice\HandleReversal
|
||||
*/
|
||||
|
||||
class ReverseInvoiceTest extends TestCase
|
||||
{
|
||||
use MakesHash;
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
public function setUp() :void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user