mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-09 03:14:30 -04:00
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:
commit
1a62a683ba
@ -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();
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -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');
|
||||
|
@ -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;
|
||||
|
@ -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',
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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
|
||||
// */
|
||||
|
@ -26,6 +26,7 @@ class Vendor extends BaseModel
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'assigned_user_id',
|
||||
'id_number',
|
||||
'vat_number',
|
||||
'work_phone',
|
||||
|
@ -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)
|
||||
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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,
|
||||
|
@ -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 [
|
||||
|
@ -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 [
|
||||
|
@ -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 ?: '',
|
||||
|
@ -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');
|
||||
|
@ -42,6 +42,7 @@ class UpdateGatewayTableVisibleColumn extends Migration
|
||||
$t->boolean('is_deleted')->default(0);
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -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()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
177
tests/Feature/CompanyGatewayResolutionTest.php
Normal file
177
tests/Feature/CompanyGatewayResolutionTest.php
Normal 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));
|
||||
}
|
||||
}
|
@ -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*/
|
||||
|
||||
|
157
tests/Feature/DocumentsApiTest.php
Normal file
157
tests/Feature/DocumentsApiTest.php
Normal 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]);
|
||||
|
||||
// }
|
||||
|
||||
}
|
@ -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,
|
||||
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user