Merge pull request #4156 from turbo124/v2

Refactor for gateway fee calculations when supporting multiple fees per payment method per gateway.
This commit is contained in:
David Bomba 2020-10-12 22:26:45 +11:00 committed by GitHub
commit 1a62a683ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 539 additions and 96 deletions

View File

@ -155,7 +155,7 @@ class PaymentController extends Controller
});
}
$payment_methods = auth()->user()->client->getPaymentMethods(array_sum(array_column($payable_invoices, 'amount_with_fee')));
//$payment_methods = auth()->user()->client->getPaymentMethods(array_sum(array_column($payable_invoices, 'amount_with_fee')));
$payment_method_id = request()->input('payment_method_id');
@ -176,7 +176,7 @@ class PaymentController extends Controller
// $fee_totals += $fee_tax;
// }
$first_invoice->service()->addGatewayFee($gateway, $invoice_totals)->save();
$first_invoice->service()->addGatewayFee($gateway, $payment_method_id, $invoice_totals)->save();
/**
*

View File

@ -467,45 +467,39 @@ class Client extends BaseModel implements HasLocalePreference
$company_gateways = $this->getSetting('company_gateway_ids');
if ($company_gateways || $company_gateways == '0') { //we need to check for "0" here as we disable a payment gateway for a client with the number "0"
//we need to check for "0" here as we disable a payment gateway for a client with the number "0"
if ($company_gateways || $company_gateways == '0') {
$transformed_ids = $this->transformKeys(explode(',', $company_gateways));
$gateways = $this->company
->company_gateways
->whereIn('id', $transformed_ids)
->sortby(function ($model) use ($transformed_ids) {
return array_search($model->id, $transformed_ids);
->sortby(function ($model) use ($transformed_ids) { //company gateways are sorted in order of priority
return array_search($model->id, $transformed_ids);// this closure sorts for us
});
} else {
$gateways = $this->company->company_gateways->where('is_deleted', false);
}
$valid_gateways = $gateways->filter(function ($method) use ($amount) {
if (isset($method->fees_and_limits)) {
//sometimes the key value of the fees and limits object are not static,
//we have to harvest the key value as follows
$properties = array_keys(get_object_vars($method->fees_and_limits));
$fees_and_limits = $method->fees_and_limits->{$properties[0]};
} else {
return true;
}
if ((property_exists($fees_and_limits, 'min_limit')) && $fees_and_limits->min_limit !== null && $fees_and_limits->min_limit != -1 && $amount < $fees_and_limits->min_limit) {
return false;
}
if ((property_exists($fees_and_limits, 'max_limit')) && $fees_and_limits->max_limit !== null && $fees_and_limits->max_limit != -1 && $amount > $fees_and_limits->max_limit) {
return false;
}
return true;
})->all();
$payment_methods = [];
foreach ($valid_gateways as $gateway) {
foreach ($gateways as $gateway) {
foreach ($gateway->driver($this)->gatewayTypes() as $type) {
$payment_methods[] = [$gateway->id => $type];
if(isset($gateway->fees_and_limits) && property_exists($gateway->fees_and_limits, $type))
{
if($this->validGatewayForAmount($gateway->fees_and_limits->{$type}, $amount))
$payment_methods[] = [$gateway->id => $type];
}
else
{
$payment_methods[] = [$gateway->id => $type];
}
}
}
@ -526,13 +520,32 @@ class Client extends BaseModel implements HasLocalePreference
'label' => ctrans('texts.'.$gateway->getTypeAlias($gateway_type_id)).$fee_label,
'company_gateway_id' => $gateway_id,
'gateway_type_id' => $gateway_type_id,
];
];
}
}
return $payment_urls;
}
public function validGatewayForAmount($fees_and_limits_for_payment_type, $amount) :bool
{
if (isset($fees_and_limits_for_payment_type)) {
$fees_and_limits = $fees_and_limits_for_payment_type;
} else {
return true;
}
if ((property_exists($fees_and_limits, 'min_limit')) && $fees_and_limits->min_limit !== null && $fees_and_limits->min_limit != -1 && $amount < $fees_and_limits->min_limit) {
return false;
}
if ((property_exists($fees_and_limits, 'max_limit')) && $fees_and_limits->max_limit !== null && $fees_and_limits->max_limit != -1 && $amount > $fees_and_limits->max_limit) {
return false;
}
return true;
}
public function preferredLocale()
{
$languages = Cache::get('languages');

View File

@ -79,10 +79,6 @@ class CompanyGateway extends BaseModel
public function getTypeAlias($gateway_type_id)
{
if ($gateway_type_id == 'token') {
$gateway_type_id = 1;
}
return GatewayType::find($gateway_type_id)->alias;
}
@ -230,19 +226,14 @@ class CompanyGateway extends BaseModel
return $this->getConfigField('publishableKey');
}
public function getFeesAndLimits()
public function getFeesAndLimits($gateway_type_id)
{
if (is_null($this->fees_and_limits)) {
if (is_null($this->fees_and_limits) || empty($this->fees_and_limits)) {
return false;
}
$fees_and_limits = new \stdClass;
return $this->fees_and_limits->{$gateway_type_id};
foreach ($this->fees_and_limits as $key => $value) {
$fees_and_limits = $this->fees_and_limits->{$key};
}
return $fees_and_limits;
}
/**
@ -252,7 +243,7 @@ class CompanyGateway extends BaseModel
* @param Client $client The client object
* @return string The fee amount formatted in the client currency
*/
public function calcGatewayFeeLabel($amount, Client $client) :string
public function calcGatewayFeeLabel($amount, Client $client, $gateway_type_id = GatewayType::CREDIT_CARD) :string
{
$label = '';
@ -260,7 +251,7 @@ class CompanyGateway extends BaseModel
return $label;
}
$fee = $this->calcGatewayFee($amount);
$fee = $this->calcGatewayFee($amount, $gateway_type_id);
if ($fee > 0) {
$fee = Number::formatMoney(round($fee, 2), $client);
@ -270,9 +261,10 @@ class CompanyGateway extends BaseModel
return $label;
}
public function calcGatewayFee($amount, $include_taxes = false)
public function calcGatewayFee($amount, $include_taxes = false, $gateway_type_id = GatewayType::CREDIT_CARD)
{
$fees_and_limits = $this->getFeesAndLimits();
$fees_and_limits = $this->getFeesAndLimits($gateway_type_id);
if (! $fees_and_limits) {
return 0;

View File

@ -20,7 +20,7 @@ class Gateway extends StaticModel
'is_offsite' => 'boolean',
'is_secure' => 'boolean',
'recommended' => 'boolean',
//'visible' => 'boolean',
'visible' => 'boolean',
'sort_order' => 'int',
'updated_at' => 'timestamp',
'created_at' => 'timestamp',

View File

@ -22,16 +22,11 @@ class GatewayType extends StaticModel
const BANK_TRANSFER = 2;
const PAYPAL = 3;
const CRYPTO = 4;
const DWOLLA = 5;
const CUSTOM1 = 6;
const ALIPAY = 7;
const SOFORT = 8;
const CUSTOM = 5;
const ALIPAY = 6;
const SOFORT = 7;
const APPLE_PAY = 8;
const SEPA = 9;
const GOCARDLESS = 10;
const APPLE_PAY = 11;
const CUSTOM2 = 12;
const CUSTOM3 = 13;
const TOKEN = 'token';
public function gateway()
{

View File

@ -61,6 +61,11 @@ class Project extends BaseModel
return $this->belongsTo(Client::class)->withTrashed();
}
public function documents()
{
return $this->morphMany(Document::class, 'documentable');
}
// /**
// * @return \Illuminate\Database\Eloquent\Relations\HasMany
// */

View File

@ -26,6 +26,7 @@ class Vendor extends BaseModel
protected $fillable = [
'name',
'assigned_user_id',
'id_number',
'vat_number',
'work_phone',

View File

@ -72,8 +72,8 @@ class BaseDriver extends AbstractPaymentDriver
* Authorize a payment method.
*
* Returns a reusable token for storage for future payments
* @param const $payment_method the GatewayType::constant
* @return view Return a view for collecting payment method information
* @param const $payment_method The GatewayType::constant
* @return view Return a view for collecting payment method information
*/
public function authorize($payment_method)
{
@ -82,8 +82,8 @@ class BaseDriver extends AbstractPaymentDriver
/**
* Executes purchase attempt for a given amount.
*
* @param float $amount The amount to be collected
* @param bool $return_client_response Whether the method needs to return a response (otherwise we assume an unattended payment)
* @param float $amount The amount to be collected
* @param bool $return_client_response Whether the method needs to return a response (otherwise we assume an unattended payment)
* @return mixed
*/
public function purchase($amount, $return_client_response = false)
@ -95,7 +95,7 @@ class BaseDriver extends AbstractPaymentDriver
*
* @param Payment $payment The Payment Object
* @param float $amount The amount to be refunded
* @param bool $return_client_response Whether the method needs to return a response (otherwise we assume an unattended payment)
* @param bool $return_client_response Whether the method needs to return a response (otherwise we assume an unattended payment)
* @return mixed
*/
public function refund(Payment $payment, $amount, $return_client_response = false)

View File

@ -102,16 +102,11 @@ class CheckoutComPaymentDriver extends BaseDriver
*/
public function viewForType($gateway_type_id)
{
//currently only ever token or creditcard so no need for switches
$this->payment_method = $gateway_type_id;
if ($gateway_type_id == GatewayType::CREDIT_CARD) {
return 'gateways.checkout.credit_card';
}
return 'gateways.checkout.credit_card';
if ($gateway_type_id == GatewayType::TOKEN) {
return 'gateways.checkout.credit_card';
}
}
/**

View File

@ -47,11 +47,11 @@ class Alipay
{
return route('client.payments.response', [
'company_gateway_id' => $this->stripe->company_gateway->id,
'gateway_type_id' => GatewayType::SOFORT,
'gateway_type_id' => GatewayType::ALIPAY,
'hashed_ids' => implode(',', $data['hashed_ids']),
'amount' => $data['amount'],
'fee' => $data['fee'],
'payment_method_id' => GatewayType::SOFORT,
'payment_method_id' => GatewayType::ALIPAY,
]);
}

View File

@ -108,7 +108,6 @@ class StripePaymentDriver extends BaseDriver
{
$types = [
GatewayType::CREDIT_CARD,
//GatewayType::TOKEN,
];
if ($this->company_gateway->getSofortEnabled() && $this->invitation && $this->client() && isset($this->client()->country) && in_array($this->client()->country, ['AUT', 'BEL', 'DEU', 'ITA', 'NLD', 'ESP'])) {
@ -142,7 +141,6 @@ class StripePaymentDriver extends BaseDriver
{
switch ($gateway_type_id) {
case GatewayType::CREDIT_CARD:
case GatewayType::TOKEN:
return 'gateways.stripe.credit_card';
break;
case GatewayType::SOFORT:

View File

@ -31,21 +31,25 @@ class AddGatewayFee extends AbstractService
private $amount;
public function __construct(CompanyGateway $company_gateway, Invoice $invoice, float $amount)
private $gateway_type_id;
public function __construct(CompanyGateway $company_gateway, int $gateway_type_id, Invoice $invoice, float $amount)
{
$this->company_gateway = $company_gateway;
$this->invoice = $invoice;
$this->amount = $amount;
$this->gateway_type_id = $gateway_type_id;
}
public function run()
{
$gateway_fee = round($this->company_gateway->calcGatewayFee($this->amount), $this->invoice->client->currency()->precision);
$gateway_fee = round($this->company_gateway->calcGatewayFee($this->amount, $this->gateway_type_id), $this->invoice->client->currency()->precision);
if((int)$gateway_fee == 0)
return;
return $this->invoice;
$this->cleanPendingGatewayFees();
@ -78,7 +82,7 @@ class AddGatewayFee extends AbstractService
$invoice_item->quantity = 1;
$invoice_item->cost = $gateway_fee;
if ($fees_and_limits = $this->company_gateway->getFeesAndLimits()) {
if ($fees_and_limits = $this->company_gateway->getFeesAndLimits($this->gateway_type_id)) {
$invoice_item->tax_rate1 = $fees_and_limits->fee_tax_rate1;
$invoice_item->tax_rate2 = $fees_and_limits->fee_tax_rate2;
$invoice_item->tax_rate3 = $fees_and_limits->fee_tax_rate3;

View File

@ -15,6 +15,7 @@ use App\DataMapper\InvoiceItem;
use App\Events\Payment\PaymentWasCreated;
use App\Factory\PaymentFactory;
use App\Models\Client;
use App\Models\Credit;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\PaymentHash;
@ -56,6 +57,9 @@ class AutoBillInvoice extends AbstractService
//if the credits cover the payments, we stop here, build the payment with credits and exit early
$this->applyCreditPayment();
info("partial = {$this->invoice->partial}");
info("balance = {$this->invoice->balance}");
/* Determine $amount */
if ($this->invoice->partial > 0)
$amount = $this->invoice->partial;
@ -64,6 +68,8 @@ class AutoBillInvoice extends AbstractService
else
return $this->finalizePaymentUsingCredits();
info("balance remains to be paid!!");
$gateway_token = $this->getGateway($amount);
/* Bail out if no payment methods available */
@ -71,7 +77,9 @@ class AutoBillInvoice extends AbstractService
return $this->invoice;
/* $gateway fee */
$fee = $gateway_token->gateway->calcGatewayFee($amount);
$fee = $gateway_token->gateway->calcGatewayFee($amount, $this->invoice->uses_inclusive_taxes);
//todo determine exact fee as per PaymentController
/* Build payment hash */
$payment_hash = PaymentHash::create([
@ -103,6 +111,7 @@ class AutoBillInvoice extends AbstractService
$payment = PaymentFactory::create($this->invoice->company_id, $this->invoice->user_id);
$payment->amount = $amount;
$payment->applied = $amount;
$payment->client_id = $this->invoice->client_id;
$payment->currency_id = $this->invoice->client->getSetting('currency_id');
$payment->date = now();
@ -113,10 +122,12 @@ class AutoBillInvoice extends AbstractService
$this->invoice->service()->setStatus(Invoice::STATUS_PAID)->save();
foreach($this->used_credit as $credit)
{
$payment->credits()->attach($credit['credit_id'], ['amount' => $credit['amount']]);
}
foreach($this->used_credit as $credit)
{
$current_credit = Credit::find($credit['credit_id']);
$payment->credits()->attach($current_credit->id, ['amount' => $credit['amount']]);
$this->applyPaymentToCredit($current_credit, $credit['amount']);
}
$payment->ledger()
->updatePaymentBalance($amount * -1)
@ -210,7 +221,7 @@ class AutoBillInvoice extends AbstractService
private function applyPaymentToCredit($credit, $amount)
private function applyPaymentToCredit($credit, $amount) :Credit
{
$credit_item = new InvoiceItem;
@ -226,7 +237,9 @@ class AutoBillInvoice extends AbstractService
$credit->line_items = $credit_items;
$credit = $credit->calc()->getCredit();
$credit->save();
return $credit;
}
/**

View File

@ -93,9 +93,9 @@ class InvoiceService
return $this;
}
public function addGatewayFee(CompanyGateway $company_gateway, float $amount)
public function addGatewayFee(CompanyGateway $company_gateway, $gateway_type_id, float $amount)
{
$this->invoice = (new AddGatewayFee($company_gateway, $this->invoice, $amount))->run();
$this->invoice = (new AddGatewayFee($company_gateway, $gateway_type_id, $this->invoice, $amount))->run();
return $this;
}

View File

@ -11,6 +11,7 @@
namespace App\Transformers;
use App\Models\Document;
use App\Models\Expense;
use App\Utils\Traits\MakesHash;
use Illuminate\Database\Eloquent\SoftDeletes;
@ -23,15 +24,23 @@ class ExpenseTransformer extends EntityTransformer
use MakesHash;
use SoftDeletes;
protected $defaultIncludes = [
'documents',
];
/**
* @var array
*/
protected $availableIncludes = [
'documents',
];
public function includeDocuments(Expense $expense)
{
$transformer = new DocumentTransformer($this->serializer);
return $this->includeCollection($expense->documents, $transformer, Document::class);
}
/**
* @param Expense $expense
*

View File

@ -95,6 +95,7 @@ class InvoiceTransformer extends EntityTransformer
'vendor_id' => (string) $this->encodePrimaryKey($invoice->vendor_id),
'status_id' => (string) ($invoice->status_id ?: 1),
'design_id' => (string) $this->encodePrimaryKey($invoice->design_id),
'recurring_id' => (string) $this->encodePrimaryKey($invoice->recurring_id),
'created_at' => (int) $invoice->created_at,
'updated_at' => (int) $invoice->updated_at,
'archived_at' => (int) $invoice->deleted_at,

View File

@ -11,6 +11,7 @@
namespace App\Transformers;
use App\Models\Document;
use App\Models\Project;
use App\Utils\Traits\MakesHash;
@ -22,14 +23,23 @@ class ProjectTransformer extends EntityTransformer
use MakesHash;
protected $defaultIncludes = [
'documents',
];
/**
* @var array
*/
protected $availableIncludes = [
'documents'
];
public function includeDocuments(Project $project)
{
$transformer = new DocumentTransformer($this->serializer);
return $this->includeCollection($project->documents, $transformer, Document::class);
}
public function transform(Project $project)
{
return [

View File

@ -11,6 +11,7 @@
namespace App\Transformers;
use App\Models\Document;
use App\Models\Task;
use App\Utils\Traits\MakesHash;
@ -22,14 +23,23 @@ class TaskTransformer extends EntityTransformer
use MakesHash;
protected $defaultIncludes = [
'documents'
];
/**
* @var array
*/
protected $availableIncludes = [
'documents'
];
public function includeDocuments(Task $task)
{
$transformer = new DocumentTransformer($this->serializer);
return $this->includeCollection($task->documents, $transformer, Document::class);
}
public function transform(Task $task)
{
return [

View File

@ -12,6 +12,7 @@
namespace App\Transformers;
use App\Models\Activity;
use App\Models\Document;
use App\Models\Vendor;
use App\Models\VendorContact;
use App\Models\VendorGatewayToken;
@ -29,6 +30,7 @@ class VendorTransformer extends EntityTransformer
protected $defaultIncludes = [
'contacts',
'documents'
];
/**
@ -36,6 +38,7 @@ class VendorTransformer extends EntityTransformer
*/
protected $availableIncludes = [
'activities',
'documents',
];
/**
@ -62,6 +65,13 @@ class VendorTransformer extends EntityTransformer
return $this->includeCollection($vendor->contacts, $transformer, VendorContact::class);
}
public function includeDocuments(Vendor $vendor)
{
$transformer = new DocumentTransformer($this->serializer);
return $this->includeCollection($vendor->documents, $transformer, Document::class);
}
/**
* @param Vendor $vendor
*
@ -85,6 +95,7 @@ class VendorTransformer extends EntityTransformer
'state' => $vendor->state ?: '',
'postal_code' => $vendor->postal_code ?: '',
'country_id' => (string) $vendor->country_id ?: '',
'currency_id' => (string) $vendor->currency_id ?: '',
'custom_value1' => $vendor->custom_value1 ?: '',
'custom_value2' => $vendor->custom_value2 ?: '',
'custom_value3' => $vendor->custom_value3 ?: '',

View File

@ -1239,6 +1239,7 @@ class CreateUsersTable extends Migration
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade')->onUpdate('cascade');
});
Schema::create('expense_categories', function ($table) {
$table->increments('id');
$table->unsignedInteger('user_id');

View File

@ -42,6 +42,7 @@ class UpdateGatewayTableVisibleColumn extends Migration
$t->boolean('is_deleted')->default(0);
});
}

View File

@ -0,0 +1,47 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class VendorSchemaUpdate extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('vendor_contacts', function ($table){
$table->timestamp('email_verified_at')->nullable();
$table->string('confirmation_code')->nullable();
$table->boolean('confirmed')->default(false);
$table->timestamp('last_login')->nullable();
$table->smallInteger('failed_logins')->nullable();
$table->string('oauth_user_id', 100)->nullable()->unique();
$table->unsignedInteger('oauth_provider_id')->nullable()->unique();
$table->string('google_2fa_secret')->nullable();
$table->string('accepted_terms_version')->nullable();
$table->string('avatar', 255)->nullable();
$table->string('avatar_type', 255)->nullable();
$table->string('avatar_size', 255)->nullable();
$table->string('password');
$table->string('token')->nullable();
$table->boolean('is_locked')->default(false);
$table->string('contact_key')->nullable();
$table->rememberToken();
$table->index(['company_id', 'email', 'deleted_at']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@ -0,0 +1,177 @@
<?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 Tests\Feature;
use App\DataMapper\FeesAndLimits;
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\CompanyGateway;
use App\Models\Credit;
use App\Models\GatewayType;
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;
use Illuminate\Support\Facades\Crypt;
/**
* @test
*/
class CompanyGatewayResolutionTest extends TestCase
{
use MakesHash;
use DatabaseTransactions;
use MockAccountData;
public $cg;
public $cg1;
public function setUp() :void
{
parent::setUp();
$this->withoutMiddleware(
ThrottleRequests::class
);
if (! config('ninja.testvars.stripe')) {
$this->markTestSkipped('Skip test no company gateways installed');
}
$this->faker = \Faker\Factory::create();
Model::reguard();
$this->makeTestData();
$this->withoutExceptionHandling();
$data = [];
$data[1]['min_limit'] = -1;
$data[1]['max_limit'] = -1;
$data[1]['fee_amount'] = 0.00;
$data[1]['fee_percent'] = 2;
$data[1]['fee_tax_name1'] = 'GST';
$data[1]['fee_tax_rate1'] = 10;
$data[1]['fee_tax_name2'] = 'GST';
$data[1]['fee_tax_rate2'] = 10;
$data[1]['fee_tax_name3'] = 'GST';
$data[1]['fee_tax_rate3'] = 10;
$data[1]['fee_cap'] = 0;
$data[2]['min_limit'] = -1;
$data[2]['max_limit'] = -1;
$data[2]['fee_amount'] = 0.00;
$data[2]['fee_percent'] = 1;
$data[2]['fee_tax_name1'] = 'GST';
$data[2]['fee_tax_rate1'] = 10;
$data[2]['fee_tax_name2'] = 'GST';
$data[2]['fee_tax_rate2'] = 10;
$data[2]['fee_tax_name3'] = 'GST';
$data[2]['fee_tax_rate3'] = 10;
$data[2]['fee_cap'] = 0;
//disable ach here
$json_config = json_decode(config('ninja.testvars.stripe'));
$json_config->enable_ach = "0";
$this->cg = new CompanyGateway;
$this->cg->company_id = $this->company->id;
$this->cg->user_id = $this->user->id;
$this->cg->gateway_key = 'd14dd26a37cecc30fdd65700bfb55b23';
$this->cg->require_cvv = true;
$this->cg->show_billing_address = true;
$this->cg->show_shipping_address = true;
$this->cg->update_details = true;
$this->cg->config = encrypt(json_encode($json_config));
$this->cg->fees_and_limits = $data;
$this->cg->save();
}
/**
* @covers \App\Models\CompanyGateway::calcGatewayFee()
*/
public function testGatewayResolution()
{
$fee = $this->cg->calcGatewayFee(10, false, GatewayType::CREDIT_CARD);
$this->assertEquals(0.2, $fee);
$fee = $this->cg->calcGatewayFee(10, false, GatewayType::BANK_TRANSFER);
$this->assertEquals(0.1, $fee);
}
/**
* @covers \App|Models\Client::validGatewayForAmount()
*/
public function testValidationForGatewayAmount()
{
$this->assertTrue($this->client->validGatewayForAmount($this->cg->fees_and_limits->{1}, 10));
$this->assertTrue($this->client->validGatewayForAmount($this->cg->fees_and_limits->{2}, 10));
}
public function testAvailablePaymentMethodsCount()
{
$amount = 10;
$payment_methods = [];
$this->assertInstanceOf("\\stdClass", $this->cg->fees_and_limits);
$this->assertObjectHasAttribute('min_limit',$this->cg->fees_and_limits->{1});
foreach ($this->cg->driver($this->client)->gatewayTypes() as $type)
{
if(property_exists($this->cg->fees_and_limits, $type))
{
if($this->client->validGatewayForAmount($this->cg->fees_and_limits->{$type}, $amount)){
$payment_methods[] = [$this->cg->id => $type];
}
}
else
{
$payment_methods[] = [$this->cg->id => $type];
}
}
$this->assertEquals(3, count($payment_methods));
}
public function testAddAchBackIntoMethods()
{
$this->assertEquals(3, count($this->cg->driver($this->client)->gatewayTypes()));
$cg_config = json_decode(decrypt($this->cg->config));
$cg_config->enable_ach = "1";
$this->cg->config = encrypt(json_encode($cg_config));
$this->cg->save();
$this->assertEquals(4, count($this->cg->driver($this->client)->gatewayTypes()));
info(print_r($this->client->getPaymentMethods(10),1));
}
}

View File

@ -11,6 +11,7 @@
namespace Tests\Feature;
use App\Models\CompanyGateway;
use App\Models\GatewayType;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\URL;
use Tests\MockAccountData;
@ -141,7 +142,7 @@ class CompanyGatewayTest extends TestCase
$balance = $this->invoice->balance;
$this->invoice = $this->invoice->service()->addGatewayFee($cg, $this->invoice->balance)->save();
$this->invoice = $this->invoice->service()->addGatewayFee($cg, GatewayType::CREDIT_CARD, $this->invoice->balance)->save();
$this->invoice = $this->invoice->calc()->getInvoice();
$items = $this->invoice->line_items;
@ -178,12 +179,12 @@ class CompanyGatewayTest extends TestCase
$total = 10.93;
$total_invoice_count = 5;
$total_gateway_fee = round($cg->calcGatewayFee($total, true), 2);
$total_gateway_fee = round($cg->calcGatewayFee($total, true, GatewayType::CREDIT_CARD), 2);
$this->assertEquals(1.58, $total_gateway_fee);
/*simple pro rata*/
$fees_and_limits = $cg->getFeesAndLimits();
$fees_and_limits = $cg->getFeesAndLimits(GatewayType::CREDIT_CARD);
/*Calculate all subcomponents of the fee*/

View File

@ -0,0 +1,157 @@
<?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 Tests\Feature;
use App\DataMapper\DefaultSettings;
use App\Models\Account;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Company;
use App\Models\User;
use App\Utils\Traits\MakesHash;
use Faker\Factory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session;
use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Http\Controllers\DocumentController
*/
class DocumentsApiTest extends TestCase
{
use MakesHash;
use DatabaseTransactions;
use MockAccountData;
public function setUp() :void
{
parent::setUp();
$this->makeTestData();
Session::start();
$this->faker = \Faker\Factory::create();
Model::reguard();
}
public function testClientDocuments()
{
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get('/api/v1/clients');
$response->assertStatus(200);
$arr = $response->json();
$this->assertArrayHasKey('documents', $arr['data'][0]);
}
public function testInvoiceDocuments()
{
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get('/api/v1/invoices');
$response->assertStatus(200);
$arr = $response->json();
$this->assertArrayHasKey('documents', $arr['data'][0]);
}
public function testProjectsDocuments()
{
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get('/api/v1/projects');
$response->assertStatus(200);
$arr = $response->json();
$this->assertArrayHasKey('documents', $arr['data'][0]);
}
public function testExpenseDocuments()
{
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get('/api/v1/expenses');
$response->assertStatus(200);
$arr = $response->json();
$this->assertArrayHasKey('documents', $arr['data'][0]);
}
public function testVendorDocuments()
{
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get('/api/v1/vendors');
$response->assertStatus(200);
$arr = $response->json();
$this->assertArrayHasKey('documents', $arr['data'][0]);
}
public function testProductDocuments()
{
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get('/api/v1/products');
$response->assertStatus(200);
$arr = $response->json();
$this->assertArrayHasKey('documents', $arr['data'][0]);
}
// public function testTaskDocuments()
// {
// $response = $this->withHeaders([
// 'X-API-SECRET' => config('ninja.api_secret'),
// 'X-API-TOKEN' => $this->token,
// ])->get('/api/v1/tasks');
// $response->assertStatus(200);
// $arr = $response->json();
// $this->assertArrayHasKey('documents', $arr['data'][0]);
// }
}

View File

@ -35,6 +35,7 @@ use App\Models\Expense;
use App\Models\GroupSetting;
use App\Models\Invoice;
use App\Models\InvoiceInvitation;
use App\Models\Product;
use App\Models\Project;
use App\Models\Quote;
use App\Models\QuoteInvitation;
@ -159,6 +160,11 @@ trait MockAccountData
$company_token->save();
Product::factory()->create([
'user_id' => $this->user->id,
'company_id' => $this->company->id,
]);
$this->client = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $this->company->id,

View File

@ -34,24 +34,19 @@ class AutoBillInvoiceTest extends TestCase
public function testAutoBillFunctionality()
{
// info("client balance = {$this->client->balance}");
// info("invoice balance = {$this->invoice->balance}");
$this->assertEquals($this->client->balance, 10);
$this->assertEquals($this->client->paid_to_date, 0);
$this->assertEquals($this->client->credit_balance, 10);
$this->invoice->service()->markSent()->autoBill()->save();
// info(print_r($this->invoice->payments()->first()->toArray(),1));
$this->assertNotNull($this->invoice->payments());
$this->assertEquals(10, $this->invoice->payments()->sum('payments.amount'));
//info(print_r($this->invoice->payments()->get(),1));
$this->assertEquals($this->client->balance, 0);
$this->assertEquals($this->client->paid_to_date, 10);
$this->assertEquals($this->client->credit_balance, 0);
}
}

View File

@ -36,6 +36,7 @@ class FeesAndLimitsTest extends TestCase
$data['fee_tax_rate2'] = '';
$data['fee_tax_name3'] = '';
$data['fee_tax_rate3'] = 0;
$data['fee_cap'] = 0;
$fees_and_limits_array = [];
$fees_and_limits_array[] = $data;