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:
David Bomba 2020-01-30 15:50:45 +11:00 committed by GitHub
parent 67c6ac1bc2
commit 63f514f3bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 269 additions and 105 deletions

View File

@ -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)

View File

@ -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(),
];

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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)

View File

@ -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
}
}
}

View File

@ -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',

View File

@ -48,9 +48,5 @@ class UploadFileTest extends TestCase
$this->assertNotNull($document);
print_r([
'relative' => generateUrl($document),
'full' => generateUrl($document, true)
]);
}
}