1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-10 05:02:36 +01:00

Implement payment rules at application edge (FormRequest) (#3202)

* Ensure payments, invoice and credit amount balance in the validator prior to saving

* additional payment validation rules and tests for processing payments

* Factories for credits

* Tests for payments

* Working on updating a payment

* Working on updating a payment

* fixes for updating a payment

* Working on Payment Tests

* More tests for payments, formrequests

* remove product_key as required from products
This commit is contained in:
David Bomba 2020-01-10 07:15:10 +11:00 committed by GitHub
parent c1c016a5b2
commit 0878decf18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 916 additions and 69 deletions

View File

@ -75,4 +75,37 @@ class InvoiceItemFactory
return $data; return $data;
} }
/**
* Generates an array of dummy data for invoice items
* @param int $items Number of line items to create
* @return array array of objects
*/
public static function generateCredit(int $items = 1) :array
{
$faker = \Faker\Factory::create();
$data = [];
for ($x=0; $x<$items; $x++) {
$item = self::create();
$item->quantity = $faker->numberBetween(-1, -10);
$item->cost = $faker->randomFloat(2, -1, -1000);
$item->line_total = $item->quantity * $item->cost;
$item->is_amount_discount = true;
$item->discount = $faker->numberBetween(1, 10);
$item->notes = $faker->realText(20);
$item->product_key = $faker->word();
$item->custom_value1 = $faker->realText(10);
$item->custom_value2 = $faker->realText(10);
$item->custom_value3 = $faker->realText(10);
$item->custom_value4 = $faker->realText(10);
$item->tax_name1 = 'GST';
$item->tax_rate1 = 10.00;
$data[] = $item;
}
return $data;
}
} }

View File

@ -412,7 +412,7 @@ class PaymentController extends BaseController
if($request->entityIsDeleted($payment)) if($request->entityIsDeleted($payment))
return $request->disallowUpdate(); return $request->disallowUpdate();
$payment = $this->payment_repo->save(request(), $payment); $payment = $this->payment_repo->save($request, $payment);
return $this->itemResponse($payment); return $this->itemResponse($payment);
} }

View File

@ -13,6 +13,8 @@ namespace App\Http\Requests\Payment;
use App\Http\Requests\Request; use App\Http\Requests\Request;
use App\Http\ValidationRules\ValidPayableInvoicesRule; use App\Http\ValidationRules\ValidPayableInvoicesRule;
use App\Http\ValidationRules\PaymentAmountsBalanceRule;
use App\Http\ValidationRules\ValidCreditsPresentRule;
use App\Models\Payment; use App\Models\Payment;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
@ -46,11 +48,20 @@ class StorePaymentRequest extends Request
} }
} }
if (isset($input['invoices']) && is_array($input['invoices']) === false) { if (isset($input['invoices']) && is_array($input['invoices']) === false) {
$input['invoices'] = null; $input['invoices'] = null;
} }
if (isset($input['credits']) && is_array($input['credits']) !== false) {
foreach ($input['credits'] as $key => $value) {
$input['credits'][$key]['id'] = $this->decodePrimaryKey($value['id']);
}
}
if (isset($input['credits']) && is_array($input['credits']) === false) {
$input['credits'] = null;
}
$this->replace($input); $this->replace($input);
} }
@ -58,10 +69,11 @@ class StorePaymentRequest extends Request
{ {
$rules = [ $rules = [
'amount' => 'numeric|required', 'amount' => 'numeric|required',
'amount' => [new PaymentAmountsBalanceRule(),new ValidCreditsPresentRule()],
'date' => 'required', 'date' => 'required',
'client_id' => 'required', 'client_id' => 'required',
'invoices' => new ValidPayableInvoicesRule(), 'invoices' => new ValidPayableInvoicesRule(),
'number' => 'nullable|unique', 'number' => 'nullable',
]; ];
return $rules; return $rules;

View File

@ -12,13 +12,16 @@
namespace App\Http\Requests\Payment; namespace App\Http\Requests\Payment;
use App\Http\Requests\Request; use App\Http\Requests\Request;
use App\Http\ValidationRules\PaymentAppliedValidAmount;
use App\Http\ValidationRules\ValidCreditsPresentRule;
use App\Utils\Traits\ChecksEntityStatus; use App\Utils\Traits\ChecksEntityStatus;
use App\Utils\Traits\MakesHash;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
class UpdatePaymentRequest extends Request class UpdatePaymentRequest extends Request
{ {
use ChecksEntityStatus; use ChecksEntityStatus;
use MakesHash;
/** /**
* Determine if the user is authorized to make this request. * Determine if the user is authorized to make this request.
* *
@ -34,8 +37,38 @@ class UpdatePaymentRequest extends Request
public function rules() public function rules()
{ {
return [ return [
'invoices' => ['required','array','min:1',new PaymentAppliedValidAmount,new ValidCreditsPresentRule],
'documents' => 'mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx', 'documents' => 'mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx',
'client_id' => 'required',
]; ];
} }
protected function prepareForValidation()
{
$input = $this->all();
if(isset($input['client_id']))
unset($input['client_id']);
if(isset($input['amount']))
unset($input['amount']);
if(isset($input['type_id']))
unset($input['type_id']);
if(isset($input['date']))
unset($input['date']);
if(isset($input['transaction_reference']))
unset($input['transaction_reference']);
if(isset($input['number']))
unset($input['number']);
if (isset($input['invoices']) && is_array($input['invoices']) !== false) {
foreach ($input['invoices'] as $key => $value) {
$input['invoices'][$key]['id'] = $this->decodePrimaryKey($value['id']);
}
}
$this->replace($input);
}
} }

View File

@ -30,7 +30,7 @@ class StoreProductRequest extends Request
public function rules() public function rules()
{ {
return [ return [
'product_key' => 'required|unique:products,product_key,null,null,company_id,'.auth()->user()->companyId(), //'product_key' => 'required|unique:products,product_key,null,null,company_id,'.auth()->user()->companyId(),
'cost' => 'numeric', 'cost' => 'numeric',
'price' => 'numeric', 'price' => 'numeric',
'quantity' => 'numeric', 'quantity' => 'numeric',

View File

@ -0,0 +1,72 @@
<?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;
use App\Libraries\MultiDB;
use App\Models\User;
use Illuminate\Contracts\Validation\Rule;
/**
* Class NewUniqueUserRule
* @package App\Http\ValidationRules
*/
class PaymentAmountsBalanceRule implements Rule
{
/**
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
return $this->calculateAmounts();
}
/**
* @return string
*/
public function message()
{
return 'Amounts do not balance correctly.';
}
private function calculateAmounts() :bool
{
$data = [];
$payment_amounts = 0;
$invoice_amounts = 0;
$payment_amounts += request()->input('amount');
if(request()->input('credits') && is_array(request()->input('credits')))
{
foreach(request()->input('credits') as $credit)
{
$payment_amounts += $credit['amount'];
}
}
if(request()->input('invoices') && is_array(request()->input('invoices')))
{
foreach(request()->input('invoices') as $invoice)
{
$invoice_amounts =+ $invoice['amount'];
}
}
else
return true; // if no invoices are present, then this is an unapplied payment, let this pass validation!
return $payment_amounts >= $invoice_amounts;
}
}

View File

@ -0,0 +1,79 @@
<?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;
use App\Libraries\MultiDB;
use App\Models\Payment;
use App\Models\User;
use App\Utils\Traits\MakesHash;
use Illuminate\Contracts\Validation\Rule;
/**
* Class PaymentAppliedValidAmount
* @package App\Http\ValidationRules
*/
class PaymentAppliedValidAmount implements Rule
{
use MakesHash;
/**
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
return $this->calculateAmounts();
}
/**
* @return string
*/
public function message()
{
return 'Insufficient applied amount remaining to cover payment.';
}
private function calculateAmounts() :bool
{
$payment = Payment::whereId($this->decodePrimaryKey(request()->segment(4)))->company()->first();
if(!$payment)
return false;
$data = [];
$payment_amounts = 0;
$invoice_amounts = 0;
$payment_amounts = $payment->amount - $payment->applied;
if(request()->input('credits') && is_array(request()->input('credits')))
{
foreach(request()->input('credits') as $credit)
{
$payment_amounts += $credit['amount'];
}
}
if(request()->input('invoices') && is_array(request()->input('invoices')))
{
foreach(request()->input('invoices') as $invoice)
{
$invoice_amounts =+ $invoice['amount'];
}
}
return $payment_amounts >= $invoice_amounts;
}
}

View File

@ -0,0 +1,65 @@
<?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;
use App\Libraries\MultiDB;
use App\Models\Credit;
use App\Models\User;
use Illuminate\Contracts\Validation\Rule;
/**
* Class ValidCreditsPresentRule
* @package App\Http\ValidationRules
*/
class ValidCreditsPresentRule implements Rule
{
/**
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
return $this->validCreditsPresent();
}
/**
* @return string
*/
public function message()
{
return 'Attempting to use one or more invalid credits';
}
private function validCreditsPresent() :bool
{
$data = [];
//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($credit['id']);
if($cred->balance >= $credit['amount'])
return false;
}
}
return true;
}
}

View File

@ -1,27 +1,27 @@
<?php <?php
/** /**
* Invoice Ninja (https://invoiceninja.com) * Credit Ninja (https://invoiceninja.com)
* *
* @link https://github.com/invoiceninja/invoiceninja source repository * @link https://github.com/invoiceninja/invoiceninja source repository
* *
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com) * @copyright Copyright (c) 2020. Credit Ninja LLC (https://invoiceninja.com)
* *
* @license https://opensource.org/licenses/AAL * @license https://opensource.org/licenses/AAL
*/ */
namespace App\Jobs\Invoice; namespace App\Jobs\Credit;
use App\Events\Payment\PaymentWasCreated; use App\Events\Payment\PaymentWasCreated;
use App\Factory\PaymentFactory; use App\Factory\PaymentFactory;
use App\Jobs\Client\UpdateClientBalance; use App\Jobs\Client\UpdateClientBalance;
use App\Jobs\Client\UpdateClientPaidToDate; use App\Jobs\Client\UpdateClientPaidToDate;
use App\Jobs\Company\UpdateCompanyLedgerWithPayment; use App\Jobs\Company\UpdateCompanyLedgerWithPayment;
use App\Jobs\Invoice\ApplyPaymentToInvoice; use App\Jobs\Credit\ApplyPaymentToCredit;
use App\Libraries\MultiDB; use App\Libraries\MultiDB;
use App\Models\Company; use App\Models\Company;
use App\Models\Invoice; use App\Models\Credit;
use App\Models\Payment; use App\Models\Payment;
use App\Repositories\InvoiceRepository; use App\Repositories\CreditRepository;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
@ -32,7 +32,7 @@ class ApplyCreditPayment implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $invoice; public $credit;
public $payment; public $payment;
@ -45,9 +45,9 @@ class ApplyCreditPayment implements ShouldQueue
* *
* @return void * @return void
*/ */
public function __construct(Invoice $invoice, Payment $payment, float $amount, Company $company) public function __construct(Credit $credit, Payment $payment, float $amount, Company $company)
{ {
$this->invoice = $invoice; $this->credit = $credit;
$this->payment = $payment; $this->payment = $payment;
$this->amount = $amount; $this->amount = $amount;
$this->company = $company; $this->company = $company;
@ -63,46 +63,25 @@ class ApplyCreditPayment implements ShouldQueue
{ {
MultiDB::setDB($this->company->db); MultiDB::setDB($this->company->db);
UpdateCompanyLedgerWithPayment::dispatchNow($this->payment, ($this->amount*-1), $this->company);
UpdateClientBalance::dispatchNow($this->payment->client, $this->amount*-1, $this->company);
UpdateClientPaidToDate::dispatchNow($this->payment->client, $this->amount, $this->company);
/* Update Pivot Record amount */ /* Update Pivot Record amount */
$this->payment->invoices->each(function ($inv) { $this->payment->credits->each(function ($cred) {
if ($inv->id == $this->invoice->id) { if ($cred->id == $this->credit->id) {
$inv->pivot->amount = $this->amount; $cred->pivot->amount = $this->amount;
$inv->pivot->save(); $cred->pivot->save();
} }
}); });
if ($this->invoice->hasPartial()) { $credit_balance = $this->credit->balance;
//is partial and amount is exactly the partial amount
if ($this->invoice->partial == $this->amount) { if ($this->amount == $credit_balance) { //total credit applied.
$this->invoice->clearPartial(); $this->credit->setStatus(Credit::STATUS_APPLIED);
$this->invoice->setDueDate(); $this->credit->updateBalance($this->amount*-1);
$this->invoice->setStatus(Invoice::STATUS_PARTIAL); } elseif($this->amount < $credit_balance) { //compare number appropriately
$this->invoice->updateBalance($this->amount*-1); $this->credit->setStatus(Credit::PARTIAL);
} elseif ($this->invoice->partial > 0 && $this->invoice->partial > $this->amount) { //partial amount exists, but the amount is less than the partial amount $this->credit->updateBalance($this->amount*-1);
$this->invoice->partial -= $this->amount;
$this->invoice->updateBalance($this->amount*-1);
} elseif ($this->invoice->partial > 0 && $this->invoice->partial < $this->amount) { //partial exists and the amount paid is GREATER than the partial amount
$this->invoice->clearPartial();
$this->invoice->setDueDate();
$this->invoice->setStatus(Invoice::STATUS_PARTIAL);
$this->invoice->updateBalance($this->amount*-1);
}
} elseif ($this->amount == $this->invoice->balance) { //total invoice paid.
$this->invoice->clearPartial();
//$this->invoice->setDueDate();
$this->invoice->setStatus(Invoice::STATUS_PAID);
$this->invoice->updateBalance($this->amount*-1);
} elseif($this->amount < $this->invoice->balance) { //partial invoice payment made
$this->invoice->clearPartial();
$this->invoice->updateBalance($this->amount*-1);
} }
/* Update Payment Applied Amount*/ /* Update Payment Applied Amount*/
$this->payment->applied += $this->amount;
$this->payment->save(); $this->payment->save();
} }

View File

@ -65,7 +65,7 @@ class ApplyInvoicePayment implements ShouldQueue
UpdateCompanyLedgerWithPayment::dispatchNow($this->payment, ($this->amount*-1), $this->company); UpdateCompanyLedgerWithPayment::dispatchNow($this->payment, ($this->amount*-1), $this->company);
UpdateClientBalance::dispatchNow($this->payment->client, $this->amount*-1, $this->company); UpdateClientBalance::dispatchNow($this->payment->client, $this->amount*-1, $this->company);
UpdateClientPaidToDate::dispatchNow($this->payment->client, $this->amount, $this->company); //UpdateClientPaidToDate::dispatchNow($this->payment->client, $this->amount, $this->company);
/* Update Pivot Record amount */ /* Update Pivot Record amount */
$this->payment->invoices->each(function ($inv) { $this->payment->invoices->each(function ($inv) {
@ -102,8 +102,8 @@ class ApplyInvoicePayment implements ShouldQueue
} }
/* Update Payment Applied Amount*/ /* Update Payment Applied Amount*/
$this->payment->applied += $this->amount; // $this->payment->applied += $this->amount;
$this->payment->save(); // $this->payment->save();
} }

View File

@ -130,4 +130,38 @@ class Credit extends BaseModel
return $credit_calc->build(); return $credit_calc->build();
} }
/**
* @param float $balance_adjustment
*/
public function updateBalance($balance_adjustment)
{
if ($this->is_deleted) {
return;
}
$balance_adjustment = floatval($balance_adjustment);
$this->balance = $this->balance + $balance_adjustment;
if ($this->balance == 0) {
$this->status_id = self::STATUS_APPLIED;
$this->save();
//event(new InvoiceWasPaid($this, $this->company));//todo
return;
}
$this->save();
}
public function setStatus($status)
{
$this->status_id = $status;
$this->save();
}
} }

View File

@ -13,6 +13,7 @@ namespace App\Repositories;
use App\Events\Payment\PaymentWasCreated; use App\Events\Payment\PaymentWasCreated;
use App\Factory\CreditFactory; use App\Factory\CreditFactory;
use App\Jobs\Client\UpdateClientPaidToDate;
use App\Jobs\Company\UpdateCompanyLedgerWithPayment; use App\Jobs\Company\UpdateCompanyLedgerWithPayment;
use App\Jobs\Invoice\ApplyClientPayment; use App\Jobs\Invoice\ApplyClientPayment;
use App\Jobs\Invoice\ApplyInvoicePayment; use App\Jobs\Invoice\ApplyInvoicePayment;
@ -45,7 +46,7 @@ class PaymentRepository extends BaseRepository
} }
/** /**
* Saves a payment. * Saves and updates a payment. //todo refactor to handle refunds and payments.
* *
* *
* @param Request $request the request object * @param Request $request the request object
@ -54,22 +55,32 @@ class PaymentRepository extends BaseRepository
*/ */
public function save(Request $request, Payment $payment) : ?Payment public function save(Request $request, Payment $payment) : ?Payment
{ {
//todo this save() only works for new payments... will fail if a Payment is updated and saved through here.
$payment->fill($request->input());
//todo this save() only works for new payments... will fail if a Payment is updated and saved through here.
$payment->fill($request->all());
$payment->status_id = Payment::STATUS_COMPLETED; $payment->status_id = Payment::STATUS_COMPLETED;
$payment->number = $payment->client->getNextPaymentNumber($payment->client);
$payment->save(); $payment->save();
if(!$payment->number)
$payment->number = $payment->client->getNextPaymentNumber($payment->client);
//we only ever update the ACTUAL amount of money transferred
UpdateClientPaidToDate::dispatchNow($payment->client, $payment->amount, $payment->company);
$invoice_totals = 0;
$credit_totals = 0;
if ($request->input('invoices') && is_array($request->input('invoices'))) { if ($request->input('invoices') && is_array($request->input('invoices'))) {
$invoice_totals = array_sum(array_column($request->input('invoices'),'amount'));
$invoices = Invoice::whereIn('id', array_column($request->input('invoices'), 'id'))->company()->get(); $invoices = Invoice::whereIn('id', array_column($request->input('invoices'), 'id'))->company()->get();
$payment->invoices()->saveMany($invoices); $payment->invoices()->saveMany($invoices);
foreach ($request->input('invoices') as $paid_invoice) { foreach ($request->input('invoices') as $paid_invoice) {
$invoice = Invoice::whereId($this->decodePrimaryKey($paid_invoice['id'])->company()->first(); $invoice = Invoice::whereId($paid_invoice['id'])->company()->first();
if ($invoice) { if ($invoice) {
ApplyInvoicePayment::dispatchNow($invoice, $payment, $paid_invoice['amount'], $invoice->company); ApplyInvoicePayment::dispatchNow($invoice, $payment, $paid_invoice['amount'], $invoice->company);
@ -82,13 +93,16 @@ class PaymentRepository extends BaseRepository
if($request->input('credits') && is_array($request->input('credits'))) if($request->input('credits') && is_array($request->input('credits')))
{ {
$credit_totals = array_sum(array_column($request->input('credits'),'amount'));
$credits = Credit::whereIn('id', array_column($request->input('credits'), 'id'))->company()->get(); $credits = Credit::whereIn('id', array_column($request->input('credits'), 'id'))->company()->get();
$payment->credits()->saveMany($credits); $payment->credits()->saveMany($credits);
foreach ($request->input('credits') as $paid_credit) foreach ($request->input('credits') as $paid_credit)
{ {
$credit = Credit::whereId($this->decodePrimaryKey($paid_credit['id'])->company()->first(); $credit = Credit::whereId($paid_credit['id'])->company()->first();
if($credit) if($credit)
ApplyCreditPayment::dispatchNow($paid_credit, $payment, $paid_credit['amount'], $credit->company); ApplyCreditPayment::dispatchNow($paid_credit, $payment, $paid_credit['amount'], $credit->company);
@ -98,7 +112,15 @@ class PaymentRepository extends BaseRepository
event(new PaymentWasCreated($payment, $payment->company)); event(new PaymentWasCreated($payment, $payment->company));
$invoice_totals -= $credit_totals;
if($invoice_totals == $payment->amount)
$payment->applied = $payment->amount;
elseif($invoice_totals < $payment->amount)
$payment->applied = $invoice_totals;
//UpdateInvoicePayment::dispatchNow($payment); //UpdateInvoicePayment::dispatchNow($payment);
$payment->save();
return $payment->fresh(); return $payment->fresh();
} }
@ -124,8 +146,22 @@ class PaymentRepository extends BaseRepository
} }
private function applyPayment(Request $request, Payment $payment) :?Payment private function applyPayment(Request $request, Payment $payment) :?Payment
{ {
$invoice_totals = array_sum(array_column($request->input('invoices'),'amount'));
$invoices = Invoice::whereIn('id', array_column($request->input('invoices'), 'id'))->company()->get();
$payment->invoices()->saveMany($invoices);
foreach ($request->input('invoices') as $paid_invoice) {
$invoice = Invoice::whereId($paid_invoice['id'])->company()->first();
if ($invoice) {
ApplyInvoicePayment::dispatchNow($invoice, $payment, $paid_invoice['amount'], $invoice->company);
}
}
} }
@ -170,6 +206,8 @@ class PaymentRepository extends BaseRepository
$client = $payment->client; $client = $payment->client;
$client->paid_to_date += $invoice_total_adjustment; $client->paid_to_date += $invoice_total_adjustment;
$payment->save();
$client->save();
} }

View File

@ -0,0 +1,32 @@
<?php
use App\DataMapper\ClientSettings;
use App\DataMapper\CompanySettings;
use App\Factory\InvoiceItemFactory;
use Faker\Generator as Faker;
$factory->define(App\Models\Credit::class, function (Faker $faker) {
return [
'status_id' => App\Models\Credit::STATUS_DRAFT,
'number' => $faker->ean13(),
'discount' => $faker->numberBetween(1,10),
'is_amount_discount' => (bool)random_int(0,1),
'tax_name1' => 'GST',
'tax_rate1' => 10,
'tax_name2' => 'VAT',
'tax_rate2' => 17.5,
//'tax_name3' => 'THIRDTAX',
//'tax_rate3' => 5,
// 'custom_value1' => $faker->numberBetween(1,4),
// 'custom_value2' => $faker->numberBetween(1,4),
// 'custom_value3' => $faker->numberBetween(1,4),
// 'custom_value4' => $faker->numberBetween(1,4),
'is_deleted' => false,
'po_number' => $faker->text(10),
'date' => $faker->date(),
'due_date' => $faker->date(),
'line_items' => InvoiceItemFactory::generateCredit(5),
'backup' => '',
'terms' => $faker->text(500),
];
});

View File

@ -100,7 +100,7 @@ class ClientApiTest extends TestCase
$arr = $response->json(); $arr = $response->json();
$this->assertNull($arr['data']['deleted_at']); $this->assertNull($arr['data']['archived_at']);
} }
public function testClientArchived() public function testClientArchived()
@ -117,7 +117,7 @@ class ClientApiTest extends TestCase
$arr = $response->json(); $arr = $response->json();
\Log::error($arr); \Log::error($arr);
$this->assertNotNull($arr['data'][0]['deleted_at']); $this->assertNotNull($arr['data'][0]['archived_at']);
} }
public function testClientRestored() public function testClientRestored()
@ -134,7 +134,7 @@ class ClientApiTest extends TestCase
$arr = $response->json(); $arr = $response->json();
$this->assertNull($arr['data'][0]['deleted_at']); $this->assertNull($arr['data'][0]['archived_at']);
} }
public function testClientDeleted() public function testClientDeleted()

View File

@ -18,7 +18,6 @@ use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker; use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session; use Illuminate\Support\Facades\Session;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use Tests\MockAccountData; use Tests\MockAccountData;
@ -35,6 +34,7 @@ class PaymentTest extends TestCase
use MakesHash; use MakesHash;
use DatabaseTransactions; use DatabaseTransactions;
use MockAccountData; use MockAccountData;
public function setUp() :void public function setUp() :void
{ {
@ -74,8 +74,9 @@ class PaymentTest extends TestCase
$client = Client::all()->first(); $client = Client::all()->first();
factory(\App\Models\Payment::class, 1)->create(['user_id' => $this->user->id, 'company_id' => $this->company->id, 'client_id' => $client->id]); factory(\App\Models\Payment::class, 1)->create(
['user_id' => $this->user->id, 'company_id' => $this->company->id, 'client_id' => $client->id]
);
$response = $this->withHeaders([ $response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'), 'X-API-SECRET' => config('ninja.api_secret'),
@ -319,7 +320,7 @@ class PaymentTest extends TestCase
catch(ValidationException $e) { catch(ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(),1); $message = json_decode($e->validator->getMessageBag(),1);
$this->assertNotNull($message); $this->assertNotNull($message);
\Log::error($message); //\Log::error($message);
} }
if($response) { if($response) {
@ -466,4 +467,472 @@ class PaymentTest extends TestCase
$this->assertEquals($invoice->balance, 8); $this->assertEquals($invoice->balance, 8);
} }
public function testPaymentValidationAmount()
{
$this->invoice = null;
$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->partial = 5.0;
$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();
$this->invoice->markSent();
$this->invoice->save();
$data = [
'amount' => 1.0,
'client_id' => $client->hashed_id,
'invoices' => [
[
'id' => $this->invoice->hashed_id,
'amount' => 2.0
],
],
'date' => '2019/12/12',
];
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->assertTrue(array_key_exists('amount', $message));
}
// if($response)
// $response->assertStatus(200);
}
public function testPaymentChangesBalancesCorrectly()
{
$this->invoice = null;
$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->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();
$this->invoice->markSent();
$this->invoice->save();
$data = [
'amount' => 2.0,
'client_id' => $client->hashed_id,
'invoices' => [
[
'id' => $this->invoice->hashed_id,
'amount' => 2.0
],
],
'date' => '2019/12/12',
];
$response = false;
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->assertTrue(array_key_exists('amount', $message));
}
if($response){
$response->assertStatus(200);
$invoice = Invoice::find($this->decodePrimaryKey($this->invoice->hashed_id));
$this->assertEquals($invoice->balance, 8);
$payment = $invoice->payments()->first();
$this->assertEquals($payment->applied, 2);
}
}
public function testUpdatePaymentValidationWorks()
{
$this->invoice = null;
$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->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();
$this->invoice->markSent();
$this->invoice->save();
$payment = PaymentFactory::create($this->company->id, $this->user->id);
$payment->amount = 10;
$payment->client_id = $client->id;
$payment->date = now();
$payment->save();
$data = [
'amount' => 2.0,
'client_id' => $client->hashed_id,
'invoices' => [],
'date' => '2019/12/12',
];
$response = false;
try {
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->put('/api/v1/payments/'.$this->encodePrimaryKey($payment->id), $data);
}
catch(ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(),1);
$this->assertTrue(array_key_exists('invoices', $message));
}
if($response)
$response->assertStatus(302);
}
public function testUpdatePaymentValidationPasses()
{
$this->invoice = null;
$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->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();
$this->invoice->markSent();
$this->invoice->save();
$payment = PaymentFactory::create($this->company->id, $this->user->id);
$payment->amount = 10;
$payment->client_id = $client->id;
$payment->date = now();
$payment->number = $client->getNextPaymentNumber($client);
$payment->save();
$data = [
'amount' => 10.0,
'client_id' => $this->encodePrimaryKey($client->id),
'invoices' => [
[
'id' => $this->encodePrimaryKey($this->invoice->id),
'amount' => 10,
]
],
'date' => '2019/12/12',
];
$response = false;
try {
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->put('/api/v1/payments/'.$this->encodePrimaryKey($payment->id), $data);
}
catch(ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(),1);
\Log::error(print_r($e->validator->getMessageBag(),1));
$this->assertTrue(array_key_exists('invoices', $message));
}
if($response)
$response->assertStatus(200);
}
public function testDoublePaymentTestWithInvalidAmounts()
{
$this->invoice = null;
$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->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();
$this->invoice->markSent();
$data = [
'amount' => 15.0,
'client_id' => $this->encodePrimaryKey($client->id),
'invoices' => [
[
'id' => $this->encodePrimaryKey($this->invoice->id),
'amount' => 10,
]
],
'date' => '2019/12/12',
];
$response = false;
try {
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/payments/', $data);
}
catch(ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(),1);
\Log::error(print_r($e->validator->getMessageBag(),1));
}
$response->assertStatus(200);
$arr = $response->json();
$payment_id = $arr['data']['id'];
$payment = Payment::find($this->decodePrimaryKey($payment_id))->first();
$this->assertEquals($payment->amount, 15);
$this->assertEquals($payment->applied, 10);
$this->invoice = null;
$this->invoice = InvoiceFactory::create($this->company->id,$this->user->id);//stub the company and user_id
$this->invoice->client_id = $client->id;
$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();
$this->invoice->markSent();
$data = [
'amount' => 15.0,
'client_id' => $this->encodePrimaryKey($client->id),
'invoices' => [
[
'id' => $this->encodePrimaryKey($this->invoice->id),
'amount' => 10,
]
],
'date' => '2019/12/12',
];
$response = false;
try {
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->put('/api/v1/payments/'.$this->encodePrimaryKey($payment->id), $data);
}
catch(ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(),1);
\Log::error(print_r($e->validator->getMessageBag(),1));
$this->assertTrue(array_key_exists('invoices', $message));
\Log::error('hit error');
}
//$response->assertStatus(302);
}
public function testDoublePaymentTestWithValidAmounts()
{
$this->invoice = null;
$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->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();
$this->invoice->markSent();
$data = [
'amount' => 20.0,
'client_id' => $this->encodePrimaryKey($client->id),
'invoices' => [
[
'id' => $this->encodePrimaryKey($this->invoice->id),
'amount' => 10,
]
],
'date' => '2019/12/12',
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/payments/', $data);
$response->assertStatus(200);
$arr = $response->json();
$payment_id = $arr['data']['id'];
$payment = Payment::find($this->decodePrimaryKey($payment_id))->first();
$this->assertEquals($payment->amount, 20);
$this->assertEquals($payment->applied, 10);
$this->invoice = null;
$this->invoice = InvoiceFactory::create($this->company->id,$this->user->id);//stub the company and user_id
$this->invoice->client_id = $client->id;
$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();
$this->invoice->markSent();
$data = [
'amount' => 20.0,
'client_id' => $this->encodePrimaryKey($client->id),
'invoices' => [
[
'id' => $this->encodePrimaryKey($this->invoice->id),
'amount' => 10,
]
],
'date' => '2019/12/12',
];
$response = false;
try {
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->put('/api/v1/payments/'.$this->encodePrimaryKey($payment->id), $data);
}
catch(ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(),1);
\Log::error(print_r($e->validator->getMessageBag(),1));
$this->assertTrue(array_key_exists('invoices', $message));
\Log::error('hit error');
}
$response->assertStatus(200);
}
} }

View File

@ -56,7 +56,7 @@ class UserTest extends TestCase
$response = $this->withHeaders([ $response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'), 'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token, 'X-API-TOKEN' => $this->token,
'X-API-PASSWORD' => 'ALongAndBriliantPassword', 'X-API-PASSWORD' => 'ALongAndBriliantPassword',
])->get('/api/v1/users'); ])->get('/api/v1/users');
$response->assertStatus(200); $response->assertStatus(200);

View File

@ -62,7 +62,7 @@ class MultiDBUserTest extends TestCase
'last_name' => 'user_db_1-s', 'last_name' => 'user_db_1-s',
'phone' => '55555', 'phone' => '55555',
'email_verified_at' => now(), 'email_verified_at' => now(),
'password' => 'ALongAndBriliantPassword', // secret 'password' => Hash::make('ALongAndBriliantPassword'), // secret
'remember_token' => \Illuminate\Support\Str::random(10), 'remember_token' => \Illuminate\Support\Str::random(10),
'email' => 'db1@example.com', 'email' => 'db1@example.com',
'oauth_user_id' => '123', 'oauth_user_id' => '123',

View File

@ -37,6 +37,7 @@ use App\Utils\Traits\GeneratesCounter;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
/** /**
@ -101,7 +102,7 @@ trait MockAccountData
if(!$this->user){ if(!$this->user){
$this->user = factory(\App\Models\User::class)->create([ $this->user = factory(\App\Models\User::class)->create([
'password' => 'ALongAndBriliantPassword', 'password' => Hash::make('ALongAndBriliantPassword'),
'confirmation_code' => $this->createDbHash(config('database.default')) 'confirmation_code' => $this->createDbHash(config('database.default'))
]); ]);
} }