mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-05-24 02:14:21 -04:00
Working on payments (#3269)
* Add PHP 7.4 to Travis Tests * Fixes for tests * fixes for tests * Fixes for tests * More tests for Refunds * Remove dusk tests * Refactor refund variables * Working on refunds * Working on refunds * working on refundS * working on refunds
This commit is contained in:
parent
67c6ac1bc2
commit
63f514f3bc
@ -13,7 +13,7 @@ group: deprecated-2017Q4
|
||||
|
||||
php:
|
||||
- 7.3
|
||||
# - 7.4
|
||||
- 7.4
|
||||
# - nightly
|
||||
|
||||
addons:
|
||||
@ -77,7 +77,7 @@ before_script:
|
||||
|
||||
script:
|
||||
- php ./vendor/bin/phpunit --debug --verbose --coverage-clover=coverage.xml
|
||||
- php artisan dusk
|
||||
#- php artisan dusk
|
||||
#- npm test
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
|
@ -67,10 +67,10 @@ class RefundPaymentRequest extends Request
|
||||
$rules = [
|
||||
'id' => 'required',
|
||||
'id' => new ValidRefundableRequest(),
|
||||
'refunded' => 'numeric',
|
||||
'amount' => 'numeric',
|
||||
'date' => 'required',
|
||||
'invoices.*.invoice_id' => 'required',
|
||||
'invoices.*.refunded' => 'required',
|
||||
'invoices.*.amount' => 'required',
|
||||
'invoices' => new ValidRefundableInvoices(),
|
||||
];
|
||||
|
||||
|
@ -67,7 +67,7 @@ class ValidRefundableRequest implements Rule
|
||||
if($payment->credits()->exists())
|
||||
{
|
||||
foreach($payment->credits as $paymentable_credit)
|
||||
$this->paymentable_type($paymentable_credit, $request_credits);
|
||||
$this->checkCredit($paymentable_credit, $request_credits);
|
||||
}
|
||||
|
||||
|
||||
@ -141,11 +141,11 @@ class ValidRefundableRequest implements Rule
|
||||
|
||||
$refundable_amount = ($paymentable->pivot->amount - $paymentable->pivot->refunded);
|
||||
|
||||
if($request_invoice['refunded'] > $refundable_amount){
|
||||
if($request_invoice['amount'] > $refundable_amount){
|
||||
|
||||
$invoice = $paymentable->paymentable;
|
||||
$invoice = $paymentable;
|
||||
|
||||
$this->error_msg = "Attempting to refund more than allowed for invoice ".$invoice->number.", maximum refundable amount is ". $refundable_amount;
|
||||
$this->error_msg = "Attempting to refund more than allowed for invoice id ".$invoice->hashed_id.", maximum refundable amount is ". $refundable_amount;
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -169,16 +169,16 @@ class ValidRefundableRequest implements Rule
|
||||
|
||||
foreach($request_credits as $request_credit)
|
||||
{
|
||||
if($request_credit['invoice_id'] == $paymentable->pivot->paymentable_id)
|
||||
if($request_credit['credit_id'] == $paymentable->pivot->paymentable_id)
|
||||
{
|
||||
|
||||
$record_found = true;
|
||||
|
||||
$refundable_amount = ($paymentable->pivot->amount - $paymentable->pivot->refunded);
|
||||
|
||||
if($request_credit['refunded'] > $refundable_amount){
|
||||
if($request_credit['amount'] > $refundable_amount){
|
||||
|
||||
$credit = $paymentable->paymentable;
|
||||
$credit = $paymentable;
|
||||
|
||||
$this->error_msg = "Attempting to refund more than allowed for credit ".$credit->number.", maximum refundable amount is ". $refundable_amount;
|
||||
return false;
|
||||
|
@ -36,7 +36,7 @@ class ValidRefundableInvoices implements Rule
|
||||
{
|
||||
$payment = Payment::whereId($this->decodePrimaryKey(request()->input('id')))->first();
|
||||
|
||||
if(request()->has('refunded') && (request()->input('refunded') > ($payment->amount - $payment->refunded))){
|
||||
if(request()->has('amount') && (request()->input('amount') > ($payment->amount - $payment->refunded))){
|
||||
$this->error_msg = "Attempting to refunded more than payment amount, enter a value equal to or lower than the payment amount of ". $payment->amount;
|
||||
return false;
|
||||
}
|
||||
@ -52,7 +52,7 @@ class ValidRefundableInvoices implements Rule
|
||||
|
||||
foreach ($invoices as $invoice) {
|
||||
if (! $invoice->isRefundable()) {
|
||||
$this->error_msg = "One or more of these invoices have been paid";
|
||||
$this->error_msg = "Invoice id ".$invoice->hashed_id ." cannot be refunded";
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -60,8 +60,11 @@ class ValidRefundableInvoices implements Rule
|
||||
foreach ($value as $val) {
|
||||
if ($val['invoice_id'] == $invoice->id) {
|
||||
|
||||
if($val['refunded'] > ($invoice->amount - $invoice->balance)){
|
||||
$this->error_msg = "Attempting to refund more than is possible for an invoice";
|
||||
//$pivot_record = $invoice->payments->where('id', $invoice->id)->first();
|
||||
$pivot_record = $payment->paymentables->where('paymentable_id', $invoice->id)->first();
|
||||
|
||||
if($val['amount'] > ($pivot_record->amount - $pivot_record->refunded)) {
|
||||
$this->error_msg = "Attempting to refund ". $val['amount'] ." only ".($pivot_record->amount - $pivot_record->refunded)." available for refund";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -163,7 +163,7 @@ class Invoice extends BaseModel
|
||||
|
||||
public function payments()
|
||||
{
|
||||
return $this->morphToMany(Payment::class, 'paymentable');
|
||||
return $this->morphToMany(Payment::class, 'paymentable')->withPivot('amount','refunded')->withTimestamps();;
|
||||
}
|
||||
|
||||
public function company_ledger()
|
||||
@ -173,7 +173,7 @@ class Invoice extends BaseModel
|
||||
|
||||
public function credits()
|
||||
{
|
||||
return $this->belongsToMany(Credit::class)->using(Paymentable::class);
|
||||
return $this->belongsToMany(Credit::class)->using(Paymentable::class)->withPivot('amount','refunded')->withTimestamps();;
|
||||
}
|
||||
|
||||
|
||||
@ -233,15 +233,15 @@ class Invoice extends BaseModel
|
||||
|
||||
public function isRefundable() : bool
|
||||
{
|
||||
// if($this->is_deleted){
|
||||
// return false;
|
||||
// } elseif ($this->balance <= 0)
|
||||
// return false;
|
||||
|
||||
if($this->is_deleted)
|
||||
return false;
|
||||
|
||||
if(($this->amount - $this->balance) == 0)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
public static function badgeForStatus(int $status)
|
||||
|
@ -6,61 +6,21 @@ use App\Factory\CreditFactory;
|
||||
use App\Factory\InvoiceItemFactory;
|
||||
use App\Models\Activity;
|
||||
use App\Models\Credit;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Payment;
|
||||
use App\Repositories\ActivityRepository;
|
||||
|
||||
trait Refundable
|
||||
{
|
||||
|
||||
//public function processRefund(array $data)
|
||||
//{
|
||||
|
||||
// if (array_key_exists('invoices', $data) && is_array($data['invoices'])) {
|
||||
|
||||
// foreach ($data['invoices'] as $adjusted_invoice) {
|
||||
|
||||
// $invoice = Invoice::whereId($adjusted_invoice['invoice_id'])->first();
|
||||
|
||||
// $invoice_total_adjustment += $adjusted_invoice['amount'];
|
||||
|
||||
// if (array_key_exists('credits', $adjusted_invoice)) {
|
||||
|
||||
// //process and insert credit notes
|
||||
// foreach ($adjusted_invoice['credits'] as $credit) {
|
||||
|
||||
// $credit = $this->credit_repo->save($credit, CreditFactory::create(auth()->user()->id, auth()->user()->id), $invoice);
|
||||
|
||||
// }
|
||||
|
||||
// } else {
|
||||
// //todo - generate Credit Note for $amount on $invoice - the assumption here is that it is a FULL refund
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
// if (array_key_exists('amount', $data) && $data['amount'] != $invoice_total_adjustment)
|
||||
// return 'Amount must equal the sum of invoice adjustments';
|
||||
// }
|
||||
|
||||
|
||||
// //adjust applied amount
|
||||
// $payment->applied += $invoice_total_adjustment;
|
||||
|
||||
// //adjust clients paid to date
|
||||
// $client = $payment->client;
|
||||
// $client->paid_to_date += $invoice_total_adjustment;
|
||||
|
||||
// $payment->save();
|
||||
// $client->save();
|
||||
//}
|
||||
|
||||
public function processRefund(array $data)
|
||||
{
|
||||
|
||||
if(isset($data['invoices']) && isset($data['credits']))
|
||||
return $this->refundPaymentWithInvoicesAndCredits($data);
|
||||
else if(isset($data['invoices']))
|
||||
return $this->refundPaymentWithInvoices($data);
|
||||
if(isset($data['invoices']))
|
||||
return $this->refundPaymentWithInvoicesAndOrCredits($data);
|
||||
|
||||
if(!isset($data['invoices']) && isset($data['credits']))
|
||||
return $this->refundPaymentWithCreditsOnly($data);
|
||||
|
||||
return $this->refundPaymentWithNoInvoicesOrCredits($data);
|
||||
}
|
||||
@ -68,35 +28,26 @@ trait Refundable
|
||||
private function refundPaymentWithNoInvoicesOrCredits(array $data)
|
||||
{
|
||||
//adjust payment refunded column amount
|
||||
$this->refunded = $data['refunded'];
|
||||
$this->refunded = $data['amount'];
|
||||
|
||||
if($data['refunded'] == $this->amount)
|
||||
if($data['amount'] == $this->amount)
|
||||
$this->status_id = Payment::STATUS_REFUNDED;
|
||||
else
|
||||
$this->status_id = Payment::STATUS_PARTIALLY_REFUNDED;
|
||||
|
||||
$credit_note = CreditFactory::create($this->company_id, $this->user_id);
|
||||
$credit_note->assigned_user_id = isset($this->assigned_user_id) ?: null;
|
||||
$credit_note->date = $data['date'];
|
||||
$credit_note->number = $this->client->getNextCreditNumber($this->client);
|
||||
$credit_note->status_id = Credit::STATUS_DRAFT;
|
||||
$credit_note->client_id = $this->client->id;
|
||||
$credit_note = $this->buildCreditNote($data);
|
||||
|
||||
$credit_line_item = InvoiceItemFactory::create();
|
||||
$credit_line_item->quantity = 1;
|
||||
$credit_line_item->cost = $data['refunded'];
|
||||
$credit_line_item->cost = $data['amount'];
|
||||
$credit_line_item->product_key = ctrans('texts.credit');
|
||||
$credit_line_item->notes = ctrans('texts.credit_created_by', ['transaction_reference', $this->number]);
|
||||
$credit_line_item->line_total = $data['refunded'];
|
||||
$credit_line_item->notes = ctrans('texts.credit_created_by', ['transaction_reference' => $this->number]);
|
||||
$credit_line_item->line_total = $data['amount'];
|
||||
$credit_line_item->date = $data['date'];
|
||||
|
||||
$line_items = [];
|
||||
$line_items[] = $credit_line_item;
|
||||
|
||||
$credit_note->line_items = $line_items;
|
||||
$credit_note->amount = $data['refunded'];
|
||||
$credit_note->balance = $data['refunded'];
|
||||
|
||||
$credit_note->save();
|
||||
|
||||
$this->createActivity($data, $credit_note->id);
|
||||
@ -104,22 +55,119 @@ trait Refundable
|
||||
//determine if we need to refund via gateway
|
||||
if($data['gateway_refund'] !== false)
|
||||
{
|
||||
//process gateway refund, on success, reduce the credit note balance to 0
|
||||
//todo process gateway refund, on success, reduce the credit note balance to 0
|
||||
}
|
||||
|
||||
|
||||
$this->save();
|
||||
|
||||
$this->client->paid_to_date -= $data['refunded'];
|
||||
//$this->client->paid_to_date -= $data['amount'];
|
||||
$this->client->save();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function refundPaymentWithInvoices($data)
|
||||
private function refundPaymentWithCreditsOnly($data)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
private function refundPaymentWithInvoicesAndOrCredits($data)
|
||||
{
|
||||
|
||||
$total_refund = 0;
|
||||
|
||||
foreach($data['invoices'] as $invoice)
|
||||
$total_refund += $invoice['amount'];
|
||||
|
||||
$data['amount'] = $total_refund;
|
||||
|
||||
if($total_refund == $this->amount)
|
||||
$this->status_id = Payment::STATUS_REFUNDED;
|
||||
else
|
||||
$this->status_id = Payment::STATUS_PARTIALLY_REFUNDED;
|
||||
|
||||
$credit_note = $this->buildCreditNote($data);
|
||||
|
||||
$line_items = [];
|
||||
|
||||
foreach($data['invoices'] as $invoice)
|
||||
{
|
||||
$inv = Invoice::find($invoice['invoice_id']);
|
||||
|
||||
$credit_line_item = InvoiceItemFactory::create();
|
||||
$credit_line_item->quantity = 1;
|
||||
$credit_line_item->cost = $invoice['amount'];
|
||||
$credit_line_item->product_key = ctrans('texts.invoice');
|
||||
$credit_line_item->notes = ctrans('texts.refund_body', ['amount' => $data['amount'], 'invoice_number' => $inv->number]);
|
||||
$credit_line_item->line_total = $invoice['amount'];
|
||||
$credit_line_item->date = $data['date'];
|
||||
|
||||
$line_items[] = $credit_line_item;
|
||||
}
|
||||
|
||||
/* Update paymentable record */
|
||||
foreach($this->invoices as $paymentable_invoice)
|
||||
{
|
||||
|
||||
foreach($data['invoices'] as $refunded_invoice)
|
||||
{
|
||||
|
||||
if($refunded_invoice['invoice_id'] == $paymentable_invoice->id)
|
||||
{
|
||||
$paymentable_invoice->pivot->refunded += $refunded_invoice['amount'];
|
||||
$paymentable_invoice->pivot->save();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
if(isset($data['credits']))
|
||||
{
|
||||
|
||||
/* Update paymentable record */
|
||||
foreach($this->credits as $paymentable_credit)
|
||||
{
|
||||
|
||||
foreach($data['credits'] as $refunded_credit)
|
||||
{
|
||||
|
||||
if($refunded_credit['credit_id'] == $refunded_credit->id)
|
||||
{
|
||||
$refunded_credit->pivot->refunded += $refunded_credit['amount'];
|
||||
$refunded_credit->pivot->save();
|
||||
|
||||
$refunded_credit->balance += $refunded_credit['amount'];
|
||||
$refunded_credit->save();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
$credit_note->line_items = $line_items;
|
||||
$credit_note->save();
|
||||
|
||||
//determine if we need to refund via gateway
|
||||
if($data['gateway_refund'] !== false)
|
||||
{
|
||||
//todo process gateway refund, on success, reduce the credit note balance to 0
|
||||
}
|
||||
|
||||
$this->save();
|
||||
|
||||
$this->adjustInvoices($data);
|
||||
|
||||
$this->client->paid_to_date -= $data['amount'];
|
||||
$this->client->save();
|
||||
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@ -128,10 +176,6 @@ trait Refundable
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function createCreditLineItems()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private function createActivity(array $data, int $credit_id)
|
||||
{
|
||||
@ -160,4 +204,43 @@ trait Refundable
|
||||
|
||||
}
|
||||
|
||||
|
||||
private function buildCreditNote(array $data) :?Credit
|
||||
{
|
||||
$credit_note = CreditFactory::create($this->company_id, $this->user_id);
|
||||
$credit_note->assigned_user_id = isset($this->assigned_user_id) ?: null;
|
||||
$credit_note->date = $data['date'];
|
||||
$credit_note->number = $this->client->getNextCreditNumber($this->client);
|
||||
$credit_note->status_id = Credit::STATUS_DRAFT;
|
||||
$credit_note->client_id = $this->client->id;
|
||||
$credit_note->amount = $data['amount'];
|
||||
$credit_note->balance = $data['amount'];
|
||||
|
||||
return $credit_note;
|
||||
}
|
||||
|
||||
private function adjustInvoices(array $data) :void
|
||||
{
|
||||
foreach($data['invoices'] as $refunded_invoice)
|
||||
{
|
||||
$invoice = Invoice::find($refunded_invoice['invoice_id']);
|
||||
|
||||
$invoice->updateBalance($refunded_invoice['amount']);
|
||||
|
||||
if($invoice->amount == $invoice->balance)
|
||||
$invoice->setStatus(Invoice::STATUS_SENT);
|
||||
else
|
||||
$invoice->setStatus(Invoice::STATUS_PARTIAL);
|
||||
|
||||
$client = $invoice->client;
|
||||
|
||||
$client->balance += $refunded_invoice['amount'];
|
||||
///$client->paid_to_date -= $refunded_invoice['amount'];
|
||||
|
||||
$client->save();
|
||||
|
||||
//todo adjust ledger balance here? or after and reference the credit and its total
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -106,7 +106,7 @@ class RefundTest extends TestCase
|
||||
|
||||
$data = [
|
||||
'id' => $this->encodePrimaryKey($payment->id),
|
||||
'refunded' => 50,
|
||||
'amount' => 50,
|
||||
// 'invoices' => [
|
||||
// [
|
||||
// 'invoice_id' => $this->invoice->hashed_id,
|
||||
@ -138,10 +138,9 @@ class RefundTest extends TestCase
|
||||
$this->assertEquals(50, $arr['data']['refunded']);
|
||||
$this->assertEquals(Payment::STATUS_REFUNDED, $arr['data']['status_id']);
|
||||
|
||||
$activity = Activity::wherePaymentId($payment->id)->first();
|
||||
// $activity = Activity::wherePaymentId($payment->id)->whereActivityTypeId(Activity::REFUNDED_PAYMENT)->first();
|
||||
|
||||
$this->assertNotNull($activity);
|
||||
$this->assertEquals(Activity::REFUNDED_PAYMENT, $activity->activity_type_id);
|
||||
// $this->assertNotNull($activity);
|
||||
}
|
||||
|
||||
public function testRefundValidationNoInvoicesProvided()
|
||||
@ -199,7 +198,7 @@ class RefundTest extends TestCase
|
||||
|
||||
$data = [
|
||||
'id' => $this->encodePrimaryKey($payment->id),
|
||||
'refunded' => 50,
|
||||
'amount' => 50,
|
||||
// 'invoices' => [
|
||||
// [
|
||||
// 'invoice_id' => $this->invoice->hashed_id,
|
||||
@ -287,11 +286,11 @@ class RefundTest extends TestCase
|
||||
|
||||
$data = [
|
||||
'id' => $this->encodePrimaryKey($payment->id),
|
||||
'refunded' => 50,
|
||||
'amount' => 50,
|
||||
'invoices' => [
|
||||
[
|
||||
'invoice_id' => $this->invoice->hashed_id,
|
||||
'refunded' => $this->invoice->amount
|
||||
'amount' => $this->invoice->amount
|
||||
],
|
||||
],
|
||||
'date' => '2020/12/12',
|
||||
@ -308,6 +307,89 @@ class RefundTest extends TestCase
|
||||
|
||||
}
|
||||
|
||||
public function testRefundValidationWithInValidInvoiceRefundedAmount()
|
||||
{
|
||||
$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();
|
||||
|
||||
$data = [
|
||||
'amount' => 50,
|
||||
'client_id' => $client->hashed_id,
|
||||
'invoices' => [
|
||||
[
|
||||
'invoice_id' => $this->invoice->hashed_id,
|
||||
'amount' => $this->invoice->amount
|
||||
],
|
||||
],
|
||||
'date' => '2020/12/12',
|
||||
|
||||
];
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->post('/api/v1/payments', $data);
|
||||
|
||||
|
||||
$arr = $response->json();
|
||||
$response->assertStatus(200);
|
||||
|
||||
$payment_id = $arr['data']['id'];
|
||||
|
||||
$this->assertEquals(50, $arr['data']['amount']);
|
||||
|
||||
$payment = Payment::whereId($this->decodePrimaryKey($payment_id))->first();
|
||||
|
||||
$this->assertNotNull($payment);
|
||||
$this->assertNotNull($payment->invoices());
|
||||
$this->assertEquals(1, $payment->invoices()->count());
|
||||
|
||||
|
||||
$data = [
|
||||
'id' => $this->encodePrimaryKey($payment->id),
|
||||
'amount' => 50,
|
||||
'invoices' => [
|
||||
[
|
||||
'invoice_id' => $this->invoice->hashed_id,
|
||||
'amount' => 100
|
||||
],
|
||||
],
|
||||
'date' => '2020/12/12',
|
||||
];
|
||||
|
||||
$response = false;
|
||||
|
||||
try{
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->post('/api/v1/payments/refund', $data);
|
||||
}catch(ValidationException $e)
|
||||
{
|
||||
$message = json_decode($e->validator->getMessageBag(),1);
|
||||
|
||||
\Log::error($message);
|
||||
}
|
||||
|
||||
if($response)
|
||||
$response->assertStatus(302);
|
||||
|
||||
}
|
||||
|
||||
public function testRefundValidationWithInValidInvoiceProvided()
|
||||
{
|
||||
@ -378,11 +460,11 @@ class RefundTest extends TestCase
|
||||
|
||||
$data = [
|
||||
'id' => $this->encodePrimaryKey($payment->id),
|
||||
'refunded' => 50,
|
||||
'amount' => 50,
|
||||
'invoices' => [
|
||||
[
|
||||
'invoice_id' => $this->invoice->hashed_id,
|
||||
'refunded' => $this->invoice->amount
|
||||
'amount' => $this->invoice->amount
|
||||
],
|
||||
],
|
||||
'date' => '2020/12/12',
|
||||
|
@ -48,9 +48,5 @@ class UploadFileTest extends TestCase
|
||||
|
||||
$this->assertNotNull($document);
|
||||
|
||||
print_r([
|
||||
'relative' => generateUrl($document),
|
||||
'full' => generateUrl($document, true)
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user