Merge remote-tracking branch 'upstream/v2' into v2-pdfmaker-design-improvements

This commit is contained in:
Benjamin Beganović 2020-09-04 13:20:39 +02:00
commit 1a66f1835f
61 changed files with 119929 additions and 119570 deletions

View File

@ -1,5 +1,5 @@
APP_NAME="Invoice Ninja" APP_NAME="Invoice Ninja"
APP_ENV=local APP_ENV=production
APP_KEY= APP_KEY=
APP_DEBUG=false APP_DEBUG=false

View File

@ -238,8 +238,13 @@ class CompanySettings extends BaseSettings
public $client_portal_terms = ''; public $client_portal_terms = '';
public $client_portal_privacy_policy = ''; public $client_portal_privacy_policy = '';
public $client_portal_enable_uploads = false; public $client_portal_enable_uploads = false;
public $client_portal_allow_under_payment = false;
public $client_portal_allow_over_payment = false;
public static $casts = [ public static $casts = [
'client_portal_allow_under_payment' => 'bool',
'client_portal_allow_over_payment' => 'bool',
'auto_bill' => 'string', 'auto_bill' => 'string',
'lock_invoices' => 'string', 'lock_invoices' => 'string',
'client_portal_terms' => 'string', 'client_portal_terms' => 'string',

View File

@ -96,7 +96,8 @@ class InvoiceController extends Controller
} }
$invoices->map(function ($invoice) { $invoices->map(function ($invoice) {
$invoice->balance = Number::formatMoney($invoice->balance, $invoice->client); $invoice->balance = Number::formatValue($invoice->balance, $invoice->client->currency());
$invoice->partial = Number::formatValue($invoice->partial, $invoice->client->currency());
return $invoice; return $invoice;
}); });
@ -113,6 +114,8 @@ class InvoiceController extends Controller
'total' => $total, 'total' => $total,
]; ];
//REFACTOR entry point for online payments starts here
return $this->render('invoices.payment', $data); return $this->render('invoices.payment', $data);
} }

View File

@ -14,16 +14,19 @@ namespace App\Http\Controllers\ClientPortal;
use App\Filters\PaymentFilters; use App\Filters\PaymentFilters;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\Jobs\Invoice\InjectSignature; use App\Jobs\Invoice\InjectSignature;
use App\Models\CompanyGateway; use App\Models\CompanyGateway;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Payment; use App\Models\Payment;
use App\Models\PaymentHash;
use App\Utils\Number; use App\Utils\Number;
use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Cache; use Cache;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Yajra\DataTables\Facades\DataTables; use Yajra\DataTables\Facades\DataTables;
/** /**
@ -72,28 +75,56 @@ class PaymentController extends Controller
*/ */
public function process() public function process()
{ {
$invoices = Invoice::whereIn('id', $this->transformKeys(request()->invoices)) //REFACTOR - Here the request will contain an array of invoices and the amount to be charged for the invoice
->where('company_id', auth('contact')->user()->company->id) //REFACTOR - At this point, we will also need to modify the invoice to include a line item for a gateway fee if applicable
->get(); // This is tagged with a type_id of 3 which is for a pending gateway fee.
//REFACTOR - In order to preserve state we should save the array of invoices and amounts and store it in db/cache and use a HASH
// to rehydrate these values in the payment response.
// dd(request()->all());
$amount = $invoices->sum('balance'); $gateway = CompanyGateway::find(request()->input('company_gateway_id'));
/*find invoices*/
$payable_invoices = request()->payable_invoices;
$invoices = Invoice::whereIn('id', $this->transformKeys(array_column($payable_invoices, 'invoice_id')))->get();
/*filter only payable invoices*/
$invoices = $invoices->filter(function ($invoice) { $invoices = $invoices->filter(function ($invoice) {
return $invoice->isPayable(); return $invoice->isPayable();
}); });
/*return early if no invoices*/
if ($invoices->count() == 0) { if ($invoices->count() == 0) {
return redirect() return redirect()
->route('client.invoices.index') ->route('client.invoices.index')
->with(['warning' => 'No payable invoices selected.']); ->with(['warning' => 'No payable invoices selected.']);
} }
$invoices->map(function ($invoice) { /*iterate through invoices and add gateway fees and other payment metadata*/
$invoice->balance = Number::formatMoney($invoice->balance, $invoice->client);
$invoice->due_date = $this->formatDate($invoice->due_date, $invoice->client->date_format()); foreach($payable_invoices as $key => $payable_invoice)
{
$payable_invoices[$key]['amount'] = Number::parseFloat($payable_invoice['amount']);
$payable_invoice['amount'] = $payable_invoices[$key]['amount'];
$invoice = $invoices->first(function ($inv) use($payable_invoice) {
return $payable_invoice['invoice_id'] == $inv->hashed_id;
});
return $invoice; $payable_invoices[$key]['due_date'] = $this->formatDate($invoice->due_date, $invoice->client->date_format());
}); $payable_invoices[$key]['invoice_number'] = $invoice->number;
if(isset($invoice->po_number))
$additional_info = $invoice->po_number;
elseif(isset($invoice->public_notes))
$additional_info = $invoice->public_notes;
else
$additional_info = $invoice->date;
$payable_invoices[$key]['additional_info'] = $additional_info;
}
if ((bool) request()->signature) { if ((bool) request()->signature) {
$invoices->each(function ($invoice) { $invoices->each(function ($invoice) {
@ -101,20 +132,46 @@ class PaymentController extends Controller
}); });
} }
$payment_methods = auth()->user()->client->getPaymentMethods($amount); $payment_methods = auth()->user()->client->getPaymentMethods(array_sum(array_column($payable_invoices, 'amount_with_fee')));
$gateway = CompanyGateway::find(request()->input('company_gateway_id'));
$payment_method_id = request()->input('payment_method_id'); $payment_method_id = request()->input('payment_method_id');
// Place to calculate gateway fee. $invoice_totals = array_sum(array_column($payable_invoices,'amount'));
$first_invoice = $invoices->first();
$fee_totals = round($gateway->calcGatewayFee($invoice_totals, true), $first_invoice->client->currency()->precision);
if(!$first_invoice->uses_inclusive_taxes) {
$fee_tax = 0;
$fee_tax += round(($first_invoice->tax_rate1/100)*$fee_totals, $first_invoice->client->currency()->precision);
$fee_tax += round(($first_invoice->tax_rate2/100)*$fee_totals, $first_invoice->client->currency()->precision);
$fee_tax += round(($first_invoice->tax_rate3/100)*$fee_totals, $first_invoice->client->currency()->precision);
$fee_totals += $fee_tax;
}
$first_invoice->service()->addGatewayFee($gateway, $invoice_totals)->save();
$payment_hash = new PaymentHash;
$payment_hash->hash = Str::random(128);
$payment_hash->data = $payable_invoices;
$payment_hash->fee_total = $fee_totals;
$payment_hash->fee_invoice_id = $first_invoice->id;
$payment_hash->save();
$totals = [
'invoice_totals' => $invoice_totals,
'fee_total' => $fee_totals,
'amount_with_fee' => $invoice_totals + $fee_totals,
];
$data = [ $data = [
'invoices' => $invoices, 'payment_hash' => $payment_hash->hash,
'amount' => $amount, 'total' => $totals,
'fee' => $gateway->calcGatewayFee($amount), 'invoices' => $payable_invoices,
'amount_with_fee' => $amount + $gateway->calcGatewayFee($amount),
'token' => auth()->user()->client->gateway_token($gateway->id, $payment_method_id), 'token' => auth()->user()->client->gateway_token($gateway->id, $payment_method_id),
'payment_method_id' => $payment_method_id, 'payment_method_id' => $payment_method_id,
'hashed_ids' => request()->invoices, 'amount_with_fee' => $invoice_totals + $fee_totals,
]; ];
return $gateway return $gateway
@ -123,10 +180,26 @@ class PaymentController extends Controller
->processPaymentView($data); ->processPaymentView($data);
} }
public function response(Request $request) public function response(PaymentResponseRequest $request)
{ {
$gateway = CompanyGateway::find($request->input('company_gateway_id')); /*Payment Gateway*/
$gateway = CompanyGateway::find($request->input('company_gateway_id'))->firstOrFail();
//REFACTOR - Entry point for the gateway response - we don't need to do anything at this point.
//
// - Inside each gateway driver, we should use have a generic code path (in BaseDriver.php)for successful/failed payment
//
// Success workflow
//
// - Rehydrate the hash and iterate through the invoices and update the balances
// - Update the type_id of the gateway fee to type_id 4
// - Link invoices to payment
//
// Failure workflow
//
// - Rehydrate hash, iterate through invoices and remove type_id 3's
// - Recalcuate invoice totals
return $gateway return $gateway
->driver(auth()->user()->client) ->driver(auth()->user()->client)
->setPaymentMethod($request->input('payment_method_id')) ->setPaymentMethod($request->input('payment_method_id'))

View File

@ -717,6 +717,10 @@ class InvoiceController extends BaseController
else else
$this->reminder_template = $invoice->calculateTemplate(); $this->reminder_template = $invoice->calculateTemplate();
//touch reminder1,2,3_sent + last_sent here if the email is a reminder.
$invoice->service()->touchReminder($this->reminder_template)->save();
$invoice->invitations->load('contact.client.country','invoice.client.country','invoice.company')->each(function ($invitation) use ($invoice) { $invoice->invitations->load('contact.client.country','invoice.client.country','invoice.company')->each(function ($invitation) use ($invoice) {
$email_builder = (new InvoiceEmail())->build($invitation, $this->reminder_template); $email_builder = (new InvoiceEmail())->build($invitation, $this->reminder_template);

View File

@ -31,7 +31,6 @@ class MigrationController extends BaseController
parent::__construct(); parent::__construct();
} }
/** /**
* *
* Purge Company * Purge Company

View File

@ -20,35 +20,37 @@ class InvoicesTable extends Component
public $status = []; public $status = [];
public function statusChange($status)
{
if (in_array($status, $this->status)) {
return $this->status = array_diff($this->status, [$status]);
}
array_push($this->status, $status);
}
public function render() public function render()
{ {
$local_status = [];
$query = Invoice::query() $query = Invoice::query()
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc'); ->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc');
if (in_array('paid', $this->status)) { if (in_array('paid', $this->status)) {
$query = $query->orWhere('status_id', Invoice::STATUS_PAID); $local_status[] = Invoice::STATUS_PAID;
} }
if (in_array('unpaid', $this->status)) { if (in_array('unpaid', $this->status)) {
$query = $query->orWhereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]); $local_status[] = Invoice::STATUS_SENT;
$local_status[] = Invoice::STATUS_PARTIAL;
} }
if (in_array('overdue', $this->status)) { if (in_array('overdue', $this->status)) {
$query = $query->orWhereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) $local_status[] = Invoice::STATUS_SENT;
->where(function ($query) { $local_status[] = Invoice::STATUS_PARTIAL;
$query }
->orWhere('due_date', '<', Carbon::now())
->orWhere('partial_due_date', '<', Carbon::now()); if (count($local_status) > 0) {
}); $query = $query->whereIn('status_id', array_unique($local_status));
}
if (in_array('overdue', $this->status)) {
$query = $query->where(function ($query) {
$query
->orWhere('due_date', '<', Carbon::now())
->orWhere('partial_due_date', '<', Carbon::now());
});
} }
$query = $query $query = $query
@ -56,6 +58,27 @@ class InvoicesTable extends Component
->where('status_id', '<>', Invoice::STATUS_DRAFT) ->where('status_id', '<>', Invoice::STATUS_DRAFT)
->paginate($this->per_page); ->paginate($this->per_page);
if (in_array('gateway_fees', $this->status)) {
$transformed = $query
->getCollection()
->filter(function ($invoice) {
$invoice['line_items'] = collect($invoice->line_items)
->filter(function ($item) {
return $item->type_id == "4" || $item->type_id == 4;
});
return count($invoice['line_items']);
});
$query = new \Illuminate\Pagination\LengthAwarePaginator(
$transformed,
$transformed->count(),
$query->perPage(),
$query->currentPage(),
['path' => request()->url(), 'query' => ['page' => $query->currentPage()]]
);
}
return render('components.livewire.invoices-table', [ return render('components.livewire.invoices-table', [
'invoices' => $query, 'invoices' => $query,
]); ]);

View File

@ -4,6 +4,7 @@ namespace App\Http\Livewire;
use App\Models\Quote; use App\Models\Quote;
use App\Utils\Traits\WithSorting; use App\Utils\Traits\WithSorting;
use Illuminate\Support\Facades\DB;
use Livewire\Component; use Livewire\Component;
use Livewire\WithPagination; use Livewire\WithPagination;
@ -15,34 +16,13 @@ class QuotesTable extends Component
public $per_page = 10; public $per_page = 10;
public $status = []; public $status = [];
public function statusChange($status)
{
if (in_array($status, $this->status)) {
return $this->status = array_diff($this->status, [$status]);
}
array_push($this->status, $status);
}
public function render() public function render()
{ {
$query = Quote::query() $query = Quote::query()
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc'); ->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc');
if (in_array('draft', $this->status)) { if (count($this->status) > 0) {
$query = $query->orWhere('status_id', Quote::STATUS_DRAFT); $query = $query->whereIn('status_id', $this->status);
}
if (in_array('sent', $this->status)) {
$query = $query->orWhere('status_id', Quote::STATUS_SENT);
}
if (in_array('approved', $this->status)) {
$query = $query->orWhere('status_id', Quote::STATUS_APPROVED);
}
if (in_array('expired', $this->status)) {
$query = $query->orWhere('status_id', Quote::STATUS_EXPIRED);
} }
$query = $query $query = $query

View File

@ -0,0 +1,39 @@
<?php
namespace App\Http\Requests\ClientPortal\Payments;
use App\Models\PaymentHash;
use Illuminate\Foundation\Http\FormRequest;
class PaymentResponseRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'company_gateway_id' => 'required',
'payment_hash' => 'required',
];
}
public function getPaymentHash()
{
$input = $this->all();
return PaymentHash::whereRaw("BINARY `hash`= ?", [$input['payment_hash']])->first();
}
}

View File

@ -13,6 +13,7 @@ namespace App\Http\Requests\CompanyGateway;
use App\Http\Requests\Request; use App\Http\Requests\Request;
use App\Http\ValidationRules\ValidCompanyGatewayFeesAndLimitsRule; use App\Http\ValidationRules\ValidCompanyGatewayFeesAndLimitsRule;
use App\Models\Gateway;
use App\Utils\Traits\CompanyGatewayFeesAndLimitsSaver; use App\Utils\Traits\CompanyGatewayFeesAndLimitsSaver;
class StoreCompanyGatewayRequest extends Request class StoreCompanyGatewayRequest extends Request
@ -42,6 +43,22 @@ class StoreCompanyGatewayRequest extends Request
protected function prepareForValidation() protected function prepareForValidation()
{ {
$input = $this->all(); $input = $this->all();
$gateway = Gateway::where('key', $input['gateway_key'])->first();
$default_gateway_fields = json_decode($gateway->fields);
/*Force gateway properties */
if(isset($input['config']) && is_object(json_decode($input['config'])))
{
foreach(json_decode($input['config']) as $key => $value) {
$default_gateway_fields->{$key} = $value;
}
$input['config'] = json_encode($default_gateway_fields);
}
if (isset($input['config'])) { if (isset($input['config'])) {
$input['config'] = encrypt($input['config']); $input['config'] = encrypt($input['config']);
@ -51,6 +68,7 @@ class StoreCompanyGatewayRequest extends Request
$input['fees_and_limits'] = $this->cleanFeesAndLimits($input['fees_and_limits']); $input['fees_and_limits'] = $this->cleanFeesAndLimits($input['fees_and_limits']);
} }
$this->replace($input); $this->replace($input);
} }
} }

View File

@ -14,6 +14,7 @@ namespace App\Http\Requests\CompanyGateway;
use App\Http\Requests\Request; use App\Http\Requests\Request;
use App\Http\ValidationRules\ValidCompanyGatewayFeesAndLimitsRule; use App\Http\ValidationRules\ValidCompanyGatewayFeesAndLimitsRule;
use App\Models\Company; use App\Models\Company;
use App\Models\Gateway;
use App\Utils\Traits\CompanyGatewayFeesAndLimitsSaver; use App\Utils\Traits\CompanyGatewayFeesAndLimitsSaver;
class UpdateCompanyGatewayRequest extends Request class UpdateCompanyGatewayRequest extends Request
@ -44,6 +45,21 @@ class UpdateCompanyGatewayRequest extends Request
{ {
$input = $this->all(); $input = $this->all();
/*Force gateway properties */
if(isset($input['config']) && is_object(json_decode($input['config'])) && array_key_exists('gateway_key', $input))
{
$gateway = Gateway::where('key', $input['gateway_key'])->first();
$default_gateway_fields = json_decode($gateway->fields);
foreach(json_decode($input['config']) as $key => $value) {
$default_gateway_fields->{$key} = $value;
}
$input['config'] = json_encode($default_gateway_fields);
}
$input['config'] = encrypt($input['config']); $input['config'] = encrypt($input['config']);
if (isset($input['fees_and_limits'])) { if (isset($input['fees_and_limits'])) {

View File

@ -91,6 +91,7 @@ class EmailInvoice extends BaseMailerJob implements ShouldQueue
catch (\Swift_TransportException $e) { catch (\Swift_TransportException $e) {
event(new InvoiceWasEmailedAndFailed($this->invoice_invitation->invoice, $this->company, $e->getMessage(), Ninja::eventVars())); event(new InvoiceWasEmailedAndFailed($this->invoice_invitation->invoice, $this->company, $e->getMessage(), Ninja::eventVars()));
} }
if (count(Mail::failures()) > 0) { if (count(Mail::failures()) > 0) {

View File

@ -227,6 +227,14 @@ class Import implements ShouldQueue
unset($data['account_id']); unset($data['account_id']);
} }
if(isset($data['referral_code'])) {
$account = $this->company->account;
$account->referral_code = $data['referral_code'];
$account->save();
unset($data['referral_code']);
}
$company_repository = new CompanyRepository(); $company_repository = new CompanyRepository();
$company_repository->save($data, $this->company); $company_repository->save($data, $this->company);

View File

@ -44,28 +44,17 @@ class QuoteUpdatedActivity implements ShouldQueue
MultiDB::setDb($event->company->db); MultiDB::setDb($event->company->db);
$quote = $event->quote; $quote = $event->quote;
$invoices = $payment->invoices;
$fields = new \stdClass; $fields = new \stdClass;
$fields->payment_id = $quote->id; $fields->quote_id = $quote->id;
$fields->client_id = $quote->client_id; $fields->client_id = $quote->client_id;
$fields->user_id = $quote->user_id; $fields->user_id = $quote->user_id;
$fields->company_id = $quote->company_id; $fields->company_id = $quote->company_id;
$fields->activity_type_id = Activity::UPDATE_QUOTE; $fields->activity_type_id = Activity::UPDATE_QUOTE;
$this->activity_repo->save($fields, $quote, $event->event_vars); $this->activity_repo->save($fields, $quote, $event->event_vars);
// foreach ($invoices as $invoice) {
// //todo we may need to add additional logic if in the future we apply payments to other entity Types, not just invoices
// $fields->invoice_id = $invoice->id;
// $this->activity_repo->save($fields, $invoice, $event->event_vars);
// }
// if (count($invoices) == 0) {
// $this->activity_repo->save($fields, $payment, $event->event_vars);
// }
} }
} }

View File

@ -393,7 +393,7 @@ class Company extends BaseModel
public function system_logs() public function system_logs()
{ {
return $this->hasMany(SystemLog::class); return $this->hasMany(SystemLog::class)->orderBy('id', 'DESC')->take(50);
} }
public function tokens_hashed() public function tokens_hashed()

View File

@ -201,6 +201,23 @@ class CompanyGateway extends BaseModel
return floatval($this->fee_amount) || floatval($this->fee_percent); return floatval($this->fee_amount) || floatval($this->fee_percent);
} }
/**
* Returns the current test mode of the gateway
*
* @return boolean whether the gateway is in testmode or not.
*/
public function isTestMode() :bool
{
$config = $this->getConfig();
if($this->gateway->provider == 'Stripe' && strpos($config->publishableKey, 'test'))
return true;
if($config && property_exists($config, 'testMode') && $config->testMode)
return true;
return false;
}
/** /**
* Get Publishable Key * Get Publishable Key
* Only works for STRIPE and PAYMILL * Only works for STRIPE and PAYMILL
@ -211,6 +228,20 @@ class CompanyGateway extends BaseModel
return $this->getConfigField('publishableKey'); return $this->getConfigField('publishableKey');
} }
public function getFeesAndLimits()
{
if (is_null($this->fees_and_limits))
return false;
$fees_and_limits = new \stdClass;
foreach($this->fees_and_limits as $key => $value) {
$fees_and_limits = $this->fees_and_limits->{$key};
}
return $fees_and_limits;
}
/** /**
* Returns the formatted fee amount for the gateway * Returns the formatted fee amount for the gateway
* *
@ -236,17 +267,13 @@ class CompanyGateway extends BaseModel
return $label; return $label;
} }
public function calcGatewayFee($amount) public function calcGatewayFee($amount, $include_taxes = false)
{ {
if (is_null($this->fees_and_limits)) {
$fees_and_limits = $this->getFeesAndLimits();
if(!$fees_and_limits)
return 0; return 0;
}
$fees_and_limits = new \stdClass;
foreach($this->fees_and_limits as $key => $value) {
$fees_and_limits = $this->fees_and_limits->{$key};
}
$fee = 0; $fee = 0;
@ -259,30 +286,68 @@ class CompanyGateway extends BaseModel
$fee += $amount * $fees_and_limits->fee_percent / 100; $fee += $amount * $fees_and_limits->fee_percent / 100;
info("fee after adding fee percent = {$fee}"); info("fee after adding fee percent = {$fee}");
} }
$pre_tax_fee = $fee;
if ($fees_and_limits->fee_tax_rate1) {
$fee += $pre_tax_fee * $fees_and_limits->fee_tax_rate1 / 100;
info("fee after adding fee tax 1 = {$fee}");
}
if ($fees_and_limits->fee_tax_rate2) {
$fee += $pre_tax_fee * $fees_and_limits->fee_tax_rate2 / 100;
info("fee after adding fee tax 2 = {$fee}");
}
if ($fees_and_limits->fee_tax_rate3) {
$fee += $pre_tax_fee * $fees_and_limits->fee_tax_rate3 / 100;
info("fee after adding fee tax 3 = {$fee}");
}
/* Cap fee if we have to here. */
if($fees_and_limits->fee_cap > 0 && ($fee > $fees_and_limits->fee_cap)) if($fees_and_limits->fee_cap > 0 && ($fee > $fees_and_limits->fee_cap))
$fee = $fees_and_limits->fee_cap; $fee = $fees_and_limits->fee_cap;
$pre_tax_fee = $fee;
/**/
if($include_taxes)
{
if ($fees_and_limits->fee_tax_rate1) {
$fee += $pre_tax_fee * $fees_and_limits->fee_tax_rate1 / 100;
info("fee after adding fee tax 1 = {$fee}");
}
if ($fees_and_limits->fee_tax_rate2) {
$fee += $pre_tax_fee * $fees_and_limits->fee_tax_rate2 / 100;
info("fee after adding fee tax 2 = {$fee}");
}
if ($fees_and_limits->fee_tax_rate3) {
$fee += $pre_tax_fee * $fees_and_limits->fee_tax_rate3 / 100;
info("fee after adding fee tax 3 = {$fee}");
}
}
return $fee; return $fee;
} }
/**
* we need to average out the gateway fees across all the invoices
* so lets iterate.
*
* we MAY need to adjust the final fee to ensure our rounding makes sense!
*/
public function calcGatewayFeeObject($amount, $invoice_count)
{
$total_gateway_fee = $this->calcGatewayFee($amount);
$fee_object = new \stdClass;
$fees_and_limits = $this->getFeesAndLimits();
if(!$fees_and_limits)
return $fee_object;
$fee_component_amount = $fees_and_limits->fee_amount ?: 0;
$fee_component_percent = $fees_and_limits->fee_percent ? ($amount * $fees_and_limits->fee_percent / 100) : 0;
$combined_fee_component = $fee_component_amount + $fee_component_percent;
$fee_component_tax_name1 = $fees_and_limits->fee_tax_name1 ?: '';
$fee_component_tax_rate1 = $fees_and_limits->fee_tax_rate1 ? ($combined_fee_component * $fees_and_limits->fee_tax_rate1 / 100) : 0;
$fee_component_tax_name2 = $fees_and_limits->fee_tax_name2 ?: '';
$fee_component_tax_rate2 = $fees_and_limits->fee_tax_rate2 ? ($combined_fee_component * $fees_and_limits->fee_tax_rate2 / 100) : 0;
$fee_component_tax_name3 = $fees_and_limits->fee_tax_name3 ?: '';
$fee_component_tax_rate3 = $fees_and_limits->fee_tax_rate3 ? ($combined_fee_component * $fees_and_limits->fee_tax_rate3 / 100) : 0;
}
public function resolveRouteBinding($value) public function resolveRouteBinding($value)
{ {
return $this return $this

View File

@ -76,6 +76,7 @@ class Payment extends BaseModel
'created_at' => 'timestamp', 'created_at' => 'timestamp',
'deleted_at' => 'timestamp', 'deleted_at' => 'timestamp',
'is_deleted' => 'bool', 'is_deleted' => 'bool',
'meta' => 'object',
]; ];
protected $with = [ protected $with = [

View File

@ -0,0 +1,29 @@
<?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\Models;
use Illuminate\Database\Eloquent\Model;
class PaymentHash extends Model
{
protected $guarded = ['id'];
protected $casts = [
'data' => 'object'
];
public function invoices()
{
return $this->data;
}
}

View File

@ -14,6 +14,8 @@ class CompanyGatewayObserver
*/ */
public function created(CompanyGateway $company_gateway) public function created(CompanyGateway $company_gateway)
{ {
/* Set company gateway if not exists*/
if(!$company_gateway->label){ if(!$company_gateway->label){
$company_gateway->label = $company_gateway->gateway->name; $company_gateway->label = $company_gateway->gateway->name;
$company_gateway->save(); $company_gateway->save();

View File

@ -18,6 +18,7 @@ use App\Jobs\Util\SystemLogger;
use App\Models\ClientGatewayToken; use App\Models\ClientGatewayToken;
use App\Models\GatewayType; use App\Models\GatewayType;
use App\Models\Payment; use App\Models\Payment;
use App\Models\PaymentHash;
use App\Models\PaymentType; use App\Models\PaymentType;
use App\Models\SystemLog; use App\Models\SystemLog;
use App\PaymentDrivers\AuthorizePaymentDriver; use App\PaymentDrivers\AuthorizePaymentDriver;
@ -46,6 +47,7 @@ class AuthorizeCreditCard
public function processPaymentView($data) public function processPaymentView($data)
{ {
$tokens = ClientGatewayToken::where('client_id', $this->authorize->client->id) $tokens = ClientGatewayToken::where('client_id', $this->authorize->client->id)
->where('company_gateway_id', $this->authorize->company_gateway->id) ->where('company_gateway_id', $this->authorize->company_gateway->id)
->where('gateway_type_id', GatewayType::CREDIT_CARD) ->where('gateway_type_id', GatewayType::CREDIT_CARD)
@ -62,6 +64,7 @@ class AuthorizeCreditCard
public function processPaymentResponse($request) public function processPaymentResponse($request)
{ {
if($request->token) if($request->token)
return $this->processTokenPayment($request); return $this->processTokenPayment($request);
@ -71,14 +74,10 @@ class AuthorizeCreditCard
$gateway_customer_reference = $authorise_create_customer->create($data); $gateway_customer_reference = $authorise_create_customer->create($data);
info($gateway_customer_reference);
$authorise_payment_method = new AuthorizePaymentMethod($this->authorize); $authorise_payment_method = new AuthorizePaymentMethod($this->authorize);
$payment_profile = $authorise_payment_method->addPaymentMethodToClient($gateway_customer_reference, $data); $payment_profile = $authorise_payment_method->addPaymentMethodToClient($gateway_customer_reference, $data);
$payment_profile_id = $payment_profile->getPaymentProfile()->getCustomerPaymentProfileId(); $payment_profile_id = $payment_profile->getPaymentProfile()->getCustomerPaymentProfileId();
info($request->input('store_card'));
if($request->has('store_card') && $request->input('store_card') === 'true'){ if($request->has('store_card') && $request->input('store_card') === 'true'){
$authorise_payment_method->payment_method = GatewayType::CREDIT_CARD; $authorise_payment_method->payment_method = GatewayType::CREDIT_CARD;
@ -93,23 +92,31 @@ class AuthorizeCreditCard
private function processTokenPayment($request) private function processTokenPayment($request)
{ {
$client_gateway_token = ClientGatewayToken::find($this->decodePrimaryKey($request->token)); $client_gateway_token = ClientGatewayToken::find($this->decodePrimaryKey($request->token));
$data = (new ChargePaymentProfile($this->authorize))->chargeCustomerProfile($client_gateway_token->gateway_customer_reference, $client_gateway_token->token, $request->input('amount_with_fee')); $data = (new ChargePaymentProfile($this->authorize))->chargeCustomerProfile($client_gateway_token->gateway_customer_reference, $client_gateway_token->token, $request->input('amount_with_fee'));
return $this->handleResponse($data, $request); return $this->handleResponse($data, $request);
} }
private function tokenBilling($cgt, $amount, $invoice) private function tokenBilling($cgt, $payment_hash)
{ {
$data = (new ChargePaymentProfile($this->authorize))->chargeCustomerProfile($cgt->gateway_customer_reference, $cgt->token, $amounts);
$amount = array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total;
$data = (new ChargePaymentProfile($this->authorize))->chargeCustomerProfile($cgt->gateway_customer_reference, $cgt->token, $amount);
if($data['response'] != null && $data['response']->getMessages()->getResultCode() == "Ok") { if($data['response'] != null && $data['response']->getMessages()->getResultCode() == "Ok") {
$payment = $this->createPaymentRecord($data, $amount); $payment = $this->createPaymentRecord($data, $amount);
$payment->meta = $cgt->meta;
$payment->save();
$this->authorize->attachInvoices($payment, $payment_hash);
$payment->service()->updateInvoicePayment($payment_hash);
$this->authorize->attachInvoices($payment, $invoice->hashed_id);
event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars())); event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars()));
$vars = [ $vars = [
@ -136,12 +143,31 @@ class AuthorizeCreditCard
private function handleResponse($data, $request) private function handleResponse($data, $request)
{ {
$response = $data['response']; $response = $data['response'];
if($response != null && $response->getMessages()->getResultCode() == "Ok") if($response != null && $response->getMessages()->getResultCode() == "Ok")
return $this->processSuccessfulResponse($data, $request); return $this->processSuccessfulResponse($data, $request);
return $this->processFailedResponse($data, $request); return $this->processFailedResponse($data, $request);
}
private function storePayment($payment_hash, $data)
{
$amount = array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total;
$payment = $this->createPaymentRecord($data, $amount);
$this->authorize->attachInvoices($payment, $payment_hash);
$payment->service()->updateInvoicePayment($payment_hash);
event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars()));
return $payment;
} }
private function createPaymentRecord($data, $amount) :?Payment private function createPaymentRecord($data, $amount) :?Payment
@ -158,21 +184,18 @@ class AuthorizeCreditCard
$payment->save(); $payment->save();
return $payment; return $payment;
} }
private function processSuccessfulResponse($data, $request) private function processSuccessfulResponse($data, $request)
{ {
$payment = $this->createPaymentRecord($data, $request->input('amount_with_fee'));
$payment_hash = PaymentHash::whereRaw("BINARY `hash`= ?", [$request->input('payment_hash')])->firstOrFail();
$this->authorize->attachInvoices($payment, $request->hashed_ids); $payment = $this->storePayment($payment_hash, $data);
$payment->service()->updateInvoicePayment();
event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars()));
$vars = [ $vars = [
'hashed_ids' => $request->input('hashed_ids'), 'invoices' => $payment_hash->invoices(),
'amount' => $request->input('amount') 'amount' => array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total
]; ];
$logger_message = [ $logger_message = [
@ -194,6 +217,7 @@ class AuthorizeCreditCard
private function formatGatewayResponse($data, $vars) private function formatGatewayResponse($data, $vars)
{ {
$response = $data['response']; $response = $data['response'];
return [ return [
@ -202,8 +226,9 @@ class AuthorizeCreditCard
'auth_code' => $response->getTransactionResponse()->getAuthCode(), 'auth_code' => $response->getTransactionResponse()->getAuthCode(),
'code' => $response->getTransactionResponse()->getMessages()[0]->getCode(), 'code' => $response->getTransactionResponse()->getMessages()[0]->getCode(),
'description' => $response->getTransactionResponse()->getMessages()[0]->getDescription(), 'description' => $response->getTransactionResponse()->getMessages()[0]->getDescription(),
'invoices' => $vars['hashed_ids'], 'invoices' => $vars['invoices'],
]; ];
} }
} }

View File

@ -122,7 +122,7 @@ class AuthorizePaymentMethod
public function createClientGatewayToken($payment_profile, $gateway_customer_reference) public function createClientGatewayToken($payment_profile, $gateway_customer_reference)
{ {
info(print_r($payment_profile,1)); // info(print_r($payment_profile,1));
$client_gateway_token = new ClientGatewayToken(); $client_gateway_token = new ClientGatewayToken();
$client_gateway_token->company_id = $this->authorize->client->company_id; $client_gateway_token->company_id = $this->authorize->client->company_id;

View File

@ -16,6 +16,7 @@ use App\Models\ClientGatewayToken;
use App\Models\GatewayType; use App\Models\GatewayType;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Payment; use App\Models\Payment;
use App\Models\PaymentHash;
use App\PaymentDrivers\Authorize\AuthorizeCreditCard; use App\PaymentDrivers\Authorize\AuthorizeCreditCard;
use App\PaymentDrivers\Authorize\AuthorizePaymentMethod; use App\PaymentDrivers\Authorize\AuthorizePaymentMethod;
use App\PaymentDrivers\Authorize\ChargePaymentProfile; use App\PaymentDrivers\Authorize\ChargePaymentProfile;
@ -143,11 +144,11 @@ class AuthorizePaymentDriver extends BaseDriver
->first(); ->first();
} }
public function tokenBilling(ClientGatewayToken $cgt, float $amount, ?Invoice $invoice = null) public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash)
{ {
$this->setPaymentMethod($cgt->gateway_type_id); $this->setPaymentMethod($cgt->gateway_type_id);
return $this->payment_method->tokenBilling($cgt, $amount, $invoice); return $this->payment_method->tokenBilling($cgt, $payment_hash);
} }
} }

View File

@ -14,11 +14,13 @@ namespace App\PaymentDrivers;
use App\Events\Invoice\InvoiceWasPaid; use App\Events\Invoice\InvoiceWasPaid;
use App\Factory\PaymentFactory; use App\Factory\PaymentFactory;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\Models\Client; use App\Models\Client;
use App\Models\ClientGatewayToken; use App\Models\ClientGatewayToken;
use App\Models\CompanyGateway; use App\Models\CompanyGateway;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Payment; use App\Models\Payment;
use App\Models\PaymentHash;
use App\PaymentDrivers\AbstractPaymentDriver; use App\PaymentDrivers\AbstractPaymentDriver;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
@ -109,25 +111,19 @@ class BaseDriver extends AbstractPaymentDriver
* @param array $hashed_ids The array of invoice hashed_ids * @param array $hashed_ids The array of invoice hashed_ids
* @return Payment The payment object * @return Payment The payment object
*/ */
public function attachInvoices(Payment $payment, $hashed_ids): Payment
public function attachInvoices(Payment $payment, PaymentHash $payment_hash): Payment
{ {
$transformed = $this->transformKeys($hashed_ids);
$array = is_array($transformed) ? $transformed : [$transformed];
$invoices = Invoice::whereIn('id', $array)
->whereClientId($this->client->id)
->get();
$paid_invoices = $payment_hash->invoices();
$invoices = Invoice::whereIn('id', $this->transformKeys(array_column($paid_invoices, 'invoice_id')))->get();
$payment->invoices()->sync($invoices); $payment->invoices()->sync($invoices);
$payment->save();
$payment->service()->applyNumber()->save();
$invoices->each(function ($invoice) use($payment){ $invoices->each(function ($invoice) use($payment){
event(new InvoiceWasPaid($invoice, $payment->company, Ninja::eventVars())); event(new InvoiceWasPaid($invoice, $payment->company, Ninja::eventVars()));
}); });
return $payment; return $payment->service()->applyNumber()->save();
} }
/** /**
@ -152,10 +148,45 @@ class BaseDriver extends AbstractPaymentDriver
/** /**
* Process an unattended payment * Process an unattended payment
* *
* @param ClientGatewayToken $cgt The client gateway token object * @param ClientGatewayToken $cgt The client gateway token object
* @param float $amount The amount to bill * @param PaymentHash $payment_hash The Payment hash containing the payment meta data
* @param Invoice $invoice Optional Invoice object being paid * @return Response The payment response
* @return Response The payment response
*/ */
public function tokenBilling(ClientGatewayToken $cgt, float $amount, ?Invoice $invoice = null) {} public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash) {}
/**
* When a successful payment is made, we need to append the gateway fee
* to an invoice
*
* @param PaymentResponseRequest $request The incoming payment request
* @return void Success/Failure
*/
public function confirmGatewayFee(PaymentResponseRequest $request) :void
{
/*Payment meta data*/
$payment_hash = $request->getPaymentHash();
/*Payment invoices*/
$payment_invoices = $payment_hash->invoices();
// /*Fee charged at gateway*/
$fee_total = $payment_hash->fee_total;
// Sum of invoice amounts
// $invoice_totals = array_sum(array_column($payment_invoices,'amount'));
/*Hydrate invoices*/
$invoices = Invoice::whereIn('id', $this->transformKeys(array_column($payment_invoices, 'invoice_id')))->get();
$invoices->each(function($invoice) use($fee_total){
if(collect($invoice->line_items)->contains('type_id', '3')){
$invoice->service()->toggleFeesPaid()->save();
$invoice->client->service()->updateBalance($fee_total)->save();
$invoice->ledger()->updateInvoiceBalance($fee_total, $notes = 'Gateway fee adjustment');
}
});
}
} }

View File

@ -13,12 +13,14 @@
namespace App\PaymentDrivers; namespace App\PaymentDrivers;
use App\Factory\PaymentFactory; use App\Factory\PaymentFactory;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\Models\Client; use App\Models\Client;
use App\Models\ClientContact; use App\Models\ClientContact;
use App\Models\CompanyGateway; use App\Models\CompanyGateway;
use App\Models\GatewayType; use App\Models\GatewayType;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Payment; use App\Models\Payment;
use App\Models\PaymentHash;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use App\Utils\Traits\SystemLogTrait; use App\Utils\Traits\SystemLogTrait;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
@ -240,14 +242,13 @@ class BasePaymentDriver
{ {
$this->gateway(); $this->gateway();
$response = $this->gateway $response = $this->gateway
->purchase($data) ->purchase($data)
->setItems($items) ->setItems($items)
->send(); ->send();
return $response; return $response;
/*
$this->purchaseResponse = (array)$response->getData();*/
} }
public function completePurchase($data) public function completePurchase($data)
@ -272,18 +273,51 @@ class BasePaymentDriver
} }
public function attachInvoices(Payment $payment, $hashed_ids): Payment public function attachInvoices(Payment $payment, PaymentHash $payment_hash): Payment
{ {
$transformed = $this->transformKeys($hashed_ids);
$array = is_array($transformed) ? $transformed : [$transformed];
$invoices = Invoice::whereIn('id', $array)
->whereClientId($this->client->id)
->get();
$paid_invoices = $payment_hash->invoices();
$invoices = Invoice::whereIn('id', $this->transformKeys(array_column($paid_invoices, 'invoice_id')))->get();
$payment->invoices()->sync($invoices); $payment->invoices()->sync($invoices);
$payment->save(); $payment->save();
return $payment; return $payment;
} }
/**
* When a successful payment is made, we need to append the gateway fee
* to an invoice
*
* @param PaymentResponseRequest $request The incoming payment request
* @return void Success/Failure
*/
public function confirmGatewayFee(PaymentResponseRequest $request) :void
{
/*Payment meta data*/
$payment_hash = $request->getPaymentHash();
/*Payment invoices*/
$payment_invoices = $payment_hash->invoices();
// /*Fee charged at gateway*/
$fee_total = $payment_hash->fee_total;
// Sum of invoice amounts
// $invoice_totals = array_sum(array_column($payment_invoices,'amount'));
/*Hydrate invoices*/
$invoices = Invoice::whereIn('id', $this->transformKeys(array_column($payment_invoices, 'invoice_id')))->get();
$invoices->each(function($invoice) use($fee_total){
if(collect($invoice->line_items)->contains('type_id', '3')){
$invoice->service()->toggleFeesPaid()->save();
$invoice->client->service()->updateBalance($fee_total)->save();
$invoice->ledger()->updateInvoiceBalance($fee_total, $notes = 'Gateway fee adjustment');
}
});
}
} }

View File

@ -18,6 +18,7 @@ use App\Jobs\Util\SystemLogger;
use App\Models\ClientGatewayToken; use App\Models\ClientGatewayToken;
use App\Models\GatewayType; use App\Models\GatewayType;
use App\Models\Payment; use App\Models\Payment;
use App\Models\PaymentHash;
use App\Models\PaymentType; use App\Models\PaymentType;
use App\Models\SystemLog; use App\Models\SystemLog;
use App\PaymentDrivers\CheckoutCom\Utilities; use App\PaymentDrivers\CheckoutCom\Utilities;
@ -109,6 +110,7 @@ class CheckoutComPaymentDriver extends BasePaymentDriver
'value' => $request->value, 'value' => $request->value,
'raw_value' => $request->raw_value, 'raw_value' => $request->raw_value,
'currency' => $request->currency, 'currency' => $request->currency,
'payment_hash' =>$request->payment_hash,
]; ];
$state = array_merge($state, $request->all()); $state = array_merge($state, $request->all());
@ -163,10 +165,9 @@ class CheckoutComPaymentDriver extends BasePaymentDriver
]; ];
$payment = $this->createPayment($data, Payment::STATUS_COMPLETED); $payment = $this->createPayment($data, Payment::STATUS_COMPLETED);
$payment_hash = PaymentHash::whereRaw("BINARY `hash`= ?", [$state['payment_hash']])->firstOrFail();
$this->attachInvoices($payment, $state['hashed_ids']); $this->attachInvoices($payment, $payment_hash);
$payment->service()->updateInvoicePayment($payment_hash);
$payment->service()->updateInvoicePayment();
event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars())); event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars()));
@ -317,6 +318,6 @@ class CheckoutComPaymentDriver extends BasePaymentDriver
} }
} }
public function tokenBilling(ClientGatewayToken $cgt, float $amount) {} public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash) {}
} }

View File

@ -18,6 +18,7 @@ use App\Jobs\Util\SystemLogger;
use App\Models\ClientGatewayToken; use App\Models\ClientGatewayToken;
use App\Models\GatewayType; use App\Models\GatewayType;
use App\Models\Payment; use App\Models\Payment;
use App\Models\PaymentHash;
use App\Models\PaymentType; use App\Models\PaymentType;
use App\Models\SystemLog; use App\Models\SystemLog;
use App\Utils\Ninja; use App\Utils\Ninja;
@ -91,7 +92,7 @@ class PayPalExpressPaymentDriver extends BasePaymentDriver
* @var $data['amount_with_fee'] * @var $data['amount_with_fee']
* @var $data['token'] * @var $data['token']
* @var $data['payment_method_id'] * @var $data['payment_method_id']
* @var $data['hashed_ids'] * @var $data['payment_hash']
* *
* @param array $data variables required to build payment page * @param array $data variables required to build payment page
* @return view Gateway and payment method specific view * @return view Gateway and payment method specific view
@ -163,10 +164,9 @@ class PayPalExpressPaymentDriver extends BasePaymentDriver
} }
$payment = $this->createPayment($response->getData()); $payment = $this->createPayment($response->getData());
$payment_hash = PaymentHash::whereRaw("BINARY `hash`= ?", [$request->input('payment_hash')])->firstOrFail();
$this->attachInvoices($payment, $request->input('hashed_ids')); $this->attachInvoices($payment, $payment_hash);
$payment->service()->updateInvoicePayment($payment_hash);
$payment->service()->UpdateInvoicePayment();
event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars())); event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars()));
@ -194,7 +194,7 @@ class PayPalExpressPaymentDriver extends BasePaymentDriver
{ {
$url = $this->client->company->domain() . "/client/payments/process/response"; $url = $this->client->company->domain() . "/client/payments/process/response";
$url .= "?company_gateway_id={$this->company_gateway->id}&gateway_type_id=" . GatewayType::PAYPAL; $url .= "?company_gateway_id={$this->company_gateway->id}&gateway_type_id=" . GatewayType::PAYPAL;
$url .= "&hashed_ids=" . implode(",", $input['hashed_ids']); $url .= "&payment_hash=" . $input['payment_hash'];
$url .= "&amount=" . $input['amount']; $url .= "&amount=" . $input['amount'];
$url .= "&fee=" . $input['fee']; $url .= "&fee=" . $input['fee'];

View File

@ -16,6 +16,7 @@ use App\Events\Payment\PaymentWasCreated;
use App\Jobs\Util\SystemLogger; use App\Jobs\Util\SystemLogger;
use App\Models\ClientGatewayToken; use App\Models\ClientGatewayToken;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\PaymentHash;
use App\Models\PaymentType; use App\Models\PaymentType;
use App\Models\SystemLog; use App\Models\SystemLog;
use App\PaymentDrivers\StripePaymentDriver; use App\PaymentDrivers\StripePaymentDriver;
@ -35,9 +36,12 @@ class Charge
* Create a charge against a payment method * Create a charge against a payment method
* @return bool success/failure * @return bool success/failure
*/ */
public function tokenBilling(ClientGatewayToken $cgt, $amount, ?Invoice $invoice) public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash)
{ {
$amount = array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total;
$invoice = sInvoice::whereIn('id', $this->transformKeys(array_column($payment_hash->invoices(), 'invoice_id')))->first();
if($invoice) if($invoice)
$description = "Invoice {$invoice->number} for {$amount} for client {$this->stripe->client->present()->name()}"; $description = "Invoice {$invoice->number} for {$amount} for client {$this->stripe->client->present()->name()}";
else else
@ -169,11 +173,12 @@ class Charge
]; ];
$payment = $this->stripe->createPaymentRecord($data, $amount); $payment = $this->stripe->createPaymentRecord($data, $amount);
$payment->meta = $cgt->meta;
$payment->save();
if($invoice) $this->stripe->attachInvoices($payment, $payment_hash);
$this->stripe->attachInvoices($payment, $invoice->hashed_id);
$payment->service()->updateInvoicePayment(); $payment->service()->updateInvoicePayment($payment_hash);
event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars())); event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars()));

View File

@ -19,6 +19,7 @@ use App\Models\ClientGatewayToken;
use App\Models\GatewayType; use App\Models\GatewayType;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Payment; use App\Models\Payment;
use App\Models\PaymentHash;
use App\Models\PaymentType; use App\Models\PaymentType;
use App\Models\SystemLog; use App\Models\SystemLog;
use App\PaymentDrivers\StripePaymentDriver; use App\PaymentDrivers\StripePaymentDriver;
@ -92,7 +93,7 @@ class CreditCard
'amount' => $this->stripe->convertToStripeAmount($data['amount_with_fee'], $this->stripe->client->currency()->precision), 'amount' => $this->stripe->convertToStripeAmount($data['amount_with_fee'], $this->stripe->client->currency()->precision),
'currency' => $this->stripe->client->getCurrencyCode(), 'currency' => $this->stripe->client->getCurrencyCode(),
'customer' => $this->stripe->findOrCreateCustomer(), 'customer' => $this->stripe->findOrCreateCustomer(),
'description' => $data['invoices']->pluck('id'), //todo more meaningful description here: 'description' => collect($data['invoices'])->pluck('id'), //todo more meaningful description here:
]; ];
if ($data['token']) { if ($data['token']) {
@ -113,6 +114,8 @@ class CreditCard
{ {
$server_response = json_decode($request->input('gateway_response')); $server_response = json_decode($request->input('gateway_response'));
$payment_hash = PaymentHash::whereRaw("BINARY `hash`= ?", [$request->input('payment_hash')])->firstOrFail();
$state = [ $state = [
'payment_method' => $server_response->payment_method, 'payment_method' => $server_response->payment_method,
'payment_status' => $server_response->status, 'payment_status' => $server_response->status,
@ -120,9 +123,11 @@ class CreditCard
'gateway_type_id' => $request->payment_method_id, 'gateway_type_id' => $request->payment_method_id,
'hashed_ids' => $request->hashed_ids, 'hashed_ids' => $request->hashed_ids,
'server_response' => $server_response, 'server_response' => $server_response,
'payment_hash' => $payment_hash,
]; ];
$invoices = Invoice::whereIn('id', $this->stripe->transformKeys($state['hashed_ids'])) /*Hydrate the invoices from the payment hash*/
$invoices = Invoice::whereIn('id', $this->stripe->transformKeys(array_column($payment_hash->invoices(), 'invoice_id')))
->whereClientId($this->stripe->client->id) ->whereClientId($this->stripe->client->id)
->get(); ->get();
@ -138,6 +143,10 @@ class CreditCard
$state['customer'] = $state['payment_intent']->customer; $state['customer'] = $state['payment_intent']->customer;
if ($state['payment_status'] == 'succeeded') { if ($state['payment_status'] == 'succeeded') {
/* Add gateway fees if needed! */
$this->stripe->confirmGatewayFee($request);
return $this->processSuccessfulPayment($state); return $this->processSuccessfulPayment($state);
} }
@ -161,6 +170,13 @@ class CreditCard
'type' => $payment_method_object['type'], 'type' => $payment_method_object['type'],
]; ];
$payment_meta = new \stdClass;
$payment_meta->exp_month = $payment_method_object['card']['exp_month'];
$payment_meta->exp_year = $payment_method_object['card']['exp_year'];
$payment_meta->brand = $payment_method_object['card']['brand'];
$payment_meta->last4 = $payment_method_object['card']['last4'];
$payment_meta->type = $payment_method_object['type'];
$payment_type = PaymentType::parseCardType($payment_method_object['card']['brand']); $payment_type = PaymentType::parseCardType($payment_method_object['card']['brand']);
if ($state['save_card'] == true) { if ($state['save_card'] == true) {
@ -180,10 +196,11 @@ class CreditCard
]; ];
$payment = $this->stripe->createPayment($data, $status = Payment::STATUS_COMPLETED); $payment = $this->stripe->createPayment($data, $status = Payment::STATUS_COMPLETED);
$payment->meta = $payment_meta;
$this->stripe->attachInvoices($payment, $state['hashed_ids']); $payment = $this->stripe->attachInvoices($payment, $state['payment_hash']);
$payment->service()->updateInvoicePayment(); $payment->service()->updateInvoicePayment($state['payment_hash']);
event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars())); event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars()));

View File

@ -23,6 +23,7 @@ use App\Models\CompanyGateway;
use App\Models\GatewayType; use App\Models\GatewayType;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Payment; use App\Models\Payment;
use App\Models\PaymentHash;
use App\Models\PaymentType; use App\Models\PaymentType;
use App\Models\SystemLog; use App\Models\SystemLog;
use App\PaymentDrivers\Stripe\ACH; use App\PaymentDrivers\Stripe\ACH;
@ -367,9 +368,9 @@ class StripePaymentDriver extends BasePaymentDriver
return response([], 200); return response([], 200);
} }
public function tokenBilling(ClientGatewayToken $cgt, float $amount, ?Invoice $invoice = null) public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash)
{ {
return (new Charge($this))->tokenBilling($cgt, $amount, $invoice); return (new Charge($this))->tokenBilling($cgt, $payment_hash);
} }
/** /**

View File

@ -0,0 +1,132 @@
<?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\Services\Invoice;
use App\DataMapper\InvoiceItem;
use App\Events\Payment\PaymentWasCreated;
use App\Factory\PaymentFactory;
use App\Models\Client;
use App\Models\CompanyGateway;
use App\Models\Invoice;
use App\Models\Payment;
use App\Services\AbstractService;
use App\Services\Client\ClientService;
use App\Services\Payment\PaymentService;
use App\Utils\Traits\GeneratesCounter;
class AddGatewayFee extends AbstractService
{
private $company_gateway;
private $invoice;
private $amount;
public function __construct(CompanyGateway $company_gateway, Invoice $invoice, float $amount)
{
$this->company_gateway = $company_gateway;
$this->invoice = $invoice;
$this->amount = $amount;
}
public function run()
{
$gateway_fee = round($this->company_gateway->calcGatewayFee($this->amount), $this->invoice->client->currency()->precision);
$this->cleanPendingGatewayFees();
if($gateway_fee > 0)
return $this->processGatewayFee($gateway_fee);
return $this->processGatewayDiscount($gateway_fee);
}
private function cleanPendingGatewayFees()
{
$invoice_items = $this->invoice->line_items;
$invoice_items = collect($invoice_items)->filter(function ($item){
return $item->type_id != '3';
});
$this->invoice->line_items = $invoice_items;
return $this;
}
private function processGatewayFee($gateway_fee)
{
$invoice_item = new InvoiceItem;
$invoice_item->type_id = '3';
$invoice_item->product_key = ctrans('texts.surcharge');
$invoice_item->notes = ctrans('texts.online_payment_surcharge');
$invoice_item->quantity = 1;
$invoice_item->cost = $gateway_fee;
if($fees_and_limits = $this->company_gateway->getFeesAndLimits())
{
$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;
}
$invoice_items = $this->invoice->line_items;
$invoice_items[] = $invoice_item;
$this->invoice->line_items = $invoice_items;
/**Refresh Invoice values*/
$this->invoice = $this->invoice->calc()->getInvoice();
/*Update client balance*/ // don't increment until we have process the payment!
//$this->invoice->client->service()->updateBalance($gateway_fee)->save();
//$this->invoice->ledger()->updateInvoiceBalance($gateway_fee, $notes = 'Gateway fee adjustment');
return $this->invoice;
}
private function processGatewayDiscount($gateway_fee)
{
$invoice_item = new InvoiceItem;
$invoice_item->type_id = '3';
$invoice_item->product_key = ctrans('texts.discount');
$invoice_item->notes = ctrans('texts.online_payment_discount');
$invoice_item->quantity = 1;
$invoice_item->cost = $gateway_fee;
if($fees_and_limits = $this->company_gateway->getFeesAndLimits())
{
$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;
}
$invoice_items = $this->invoice->line_items;
$invoice_items[] = $invoice_item;
$this->invoice->line_items = $invoice_items;
$this->invoice = $this->invoice->calc()->getInvoice();
// $this->invoice->client->service()->updateBalance($gateway_fee)->save();
// $this->invoice->ledger()->updateInvoiceBalance($gateway_fee, $notes = 'Discount fee adjustment');
return $this->invoice;
}
}

View File

@ -17,10 +17,12 @@ use App\Factory\PaymentFactory;
use App\Models\Client; use App\Models\Client;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Payment; use App\Models\Payment;
use App\Models\PaymentHash;
use App\Services\AbstractService; use App\Services\AbstractService;
use App\Services\Client\ClientService; use App\Services\Client\ClientService;
use App\Services\Payment\PaymentService; use App\Services\Payment\PaymentService;
use App\Utils\Traits\GeneratesCounter; use App\Utils\Traits\GeneratesCounter;
use Illuminate\Support\Str;
class AutoBillInvoice extends AbstractService class AutoBillInvoice extends AbstractService
{ {
@ -55,31 +57,39 @@ class AutoBillInvoice extends AbstractService
if($this->invoice->partial > 0){ if($this->invoice->partial > 0){
$fee = $gateway_token->gateway->calcGatewayFee($this->invoice->partial); $fee = $gateway_token->gateway->calcGatewayFee($this->invoice->partial);
$amount = $this->invoice->partial + $fee; // $amount = $this->invoice->partial + $fee;
$amount = $this->invoice->partial;
} }
else{ else{
$fee = $gateway_token->gateway->calcGatewayFee($this->invoice->balance); $fee = $gateway_token->gateway->calcGatewayFee($this->invoice->balance);
$amount = $this->invoice->balance + $fee; // $amount = $this->invoice->balance + $fee;
$amount = $this->invoice->balance;
} }
/* Make sure we remove any stale fees*/ $payment_hash = PaymentHash::create([
$this->purgeStaleGatewayFees(); 'hash' => Str::random(128),
'data' => ['invoice_id' => $this->invoice->hashed_id, 'amount' => $amount],
'fee_total' => $fee,
'fee_invoice_id' => $this->invoice->id,
]);
if($fee > 0) $payment = $gateway_token->gateway->driver($this->client)->tokenBilling($gateway_token, $payment_hash);
$this->addFeeToInvoice($fee);
$payment = $gateway_token->gateway->driver($this->client)->tokenBilling($gateway_token, $amount, $this->invoice); //this is redundant - taken care of much further down.
// if($payment){
if($payment){ // if($this->invoice->partial > 0)
// $amount = $this->invoice->partial;
$this->invoice = $this->invoice->service()->toggleFeesPaid()->save(); // else
// $amount = $this->invoice->balance;
} // $this->invoice = $this->invoice->service()->addGatewayFee($gateway_token->gateway, $amount)->save();
else
{
//TODO autobill failed
}
// }
// else
// {
// //TODO autobill failed
// }
return $this->invoice; return $this->invoice;
} }
@ -144,34 +154,34 @@ class AutoBillInvoice extends AbstractService
* *
* @return $this * @return $this
*/ */
private function purgeStaleGatewayFees() // private function purgeStaleGatewayFees()
{ // {
$starting_amount = $this->invoice->amount; // $starting_amount = $this->invoice->amount;
$line_items = $this->invoice->line_items; // $line_items = $this->invoice->line_items;
$new_items = []; // $new_items = [];
foreach($line_items as $item) // foreach($line_items as $item)
{ // {
if($item->type_id != 3) // if($item->type_id != 3)
$new_items[] = $item; // $new_items[] = $item;
} // }
$this->invoice->line_items = $new_items; // $this->invoice->line_items = $new_items;
$this->invoice->save(); // $this->invoice->save();
$this->invoice = $this->invoice->calc()->getInvoice(); // $this->invoice = $this->invoice->calc()->getInvoice();
if($starting_amount != $this->invoice->amount && $this->invoice->status_id != Invoice::STATUS_DRAFT){ // if($starting_amount != $this->invoice->amount && $this->invoice->status_id != Invoice::STATUS_DRAFT){
$this->invoice->client->service()->updateBalance($this->invoice->amount - $starting_amount)->save(); // $this->invoice->client->service()->updateBalance($this->invoice->amount - $starting_amount)->save();
$this->invoice->ledger()->updateInvoiceBalance($this->invoice->amount - $starting_amount, 'Invoice balance updated after stale gateway fee removed')->save(); // $this->invoice->ledger()->updateInvoiceBalance($this->invoice->amount - $starting_amount, 'Invoice balance updated after stale gateway fee removed')->save();
} // }
return $this; // return $this;
} // }
/** /**
* Checks whether a given gateway token is able * Checks whether a given gateway token is able

View File

@ -11,6 +11,7 @@
namespace App\Services\Invoice; namespace App\Services\Invoice;
use App\Models\CompanyGateway;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Payment; use App\Models\Payment;
use App\Services\Client\ClientService; use App\Services\Client\ClientService;
@ -76,11 +77,20 @@ class InvoiceService
return $this; return $this;
} }
public function addGatewayFee(CompanyGateway $company_gateway, float $amount)
{
$this->invoice = (new AddGatewayFee($company_gateway, $this->invoice, $amount))->run();
return $this;
}
/** /**
* Update an invoice balance * Update an invoice balance
*
* @param float $balance_adjustment The amount to adjust the invoice by * @param float $balance_adjustment The amount to adjust the invoice by
* a negative amount will REDUCE the invoice balance, a positive amount will INCREASE * a negative amount will REDUCE the invoice balance, a positive amount will INCREASE
* the invoice balance * the invoice balance
*
* @return InvoiceService Parent class object * @return InvoiceService Parent class object
*/ */
public function updateBalance($balance_adjustment) public function updateBalance($balance_adjustment)
@ -182,8 +192,8 @@ class InvoiceService
$this->invoice->line_items = collect($this->invoice->line_items) $this->invoice->line_items = collect($this->invoice->line_items)
->map(function ($item) { ->map(function ($item) {
if($item->type_id == 3) if($item->type_id == '3')
$item->type_id = 4; $item->type_id = '4';
return $item; return $item;
@ -198,13 +208,14 @@ class InvoiceService
$this->invoice->line_items = collect($this->invoice->line_items) $this->invoice->line_items = collect($this->invoice->line_items)
->reject(function ($item) { ->reject(function ($item) {
return $item->type_id == 3; return $item->type_id == '3';
})->toArray(); })->toArray();
return $this; return $this;
} }
/*Set partial value and due date to null*/
public function clearPartial() public function clearPartial()
{ {
$this->invoice->partial = null; $this->invoice->partial = null;
@ -213,6 +224,7 @@ class InvoiceService
return $this; return $this;
} }
/*Update the partial amount of a invoice*/
public function updatePartial($amount) public function updatePartial($amount)
{ {
$this->invoice->partial += $amount; $this->invoice->partial += $amount;
@ -220,6 +232,31 @@ class InvoiceService
return $this; return $this;
} }
/*When a reminder is sent we want to touch the dates they were sent*/
public function touchReminder(string $reminder_template)
{
switch ($reminder_template) {
case 'reminder1':
$this->invoice->reminder1_sent = now()->format('Y-m-d');
$this->invoice->reminder_last_sent = now()->format('Y-m-d');
break;
case 'reminder2':
$this->invoice->reminder2_sent = now()->format('Y-m-d');
$this->invoice->reminder_last_sent = now()->format('Y-m-d');
break;
case 'reminder3':
$this->invoice->reminder3_sent = now()->format('Y-m-d');
$this->invoice->reminder_last_sent = now()->format('Y-m-d');
break;
default:
# code...
break;
}
return $this;
}
/** /**

View File

@ -56,6 +56,10 @@ class TriggeredActions extends AbstractService
$this->sendEmail(); $this->sendEmail();
} }
if($this->request->has('mark_sent') && $this->request->input('mark_sent') == 'true'){
$this->invoice = $this->invoice->service()->markSent()->save();
}
return $this->invoice; return $this->invoice;
} }

View File

@ -37,8 +37,6 @@ class UpdateBalance extends AbstractService
if ($this->invoice->balance == 0) { if ($this->invoice->balance == 0) {
$this->invoice->status_id = Invoice::STATUS_PAID; $this->invoice->status_id = Invoice::STATUS_PAID;
// $this->save();
// event(new InvoiceWasPaid($this, $this->company));
} }
return $this->invoice; return $this->invoice;

View File

@ -84,9 +84,9 @@ class PaymentService
return (new DeletePayment($this->payment))->run(); return (new DeletePayment($this->payment))->run();
} }
public function updateInvoicePayment() :?Payment public function updateInvoicePayment($payment_hash = null) :?Payment
{ {
return ((new UpdateInvoicePayment($this->payment)))->run(); return ((new UpdateInvoicePayment($this->payment, $payment_hash)))->run();
} }
public function applyNumber() public function applyNumber()

View File

@ -12,125 +12,101 @@
namespace App\Services\Payment; namespace App\Services\Payment;
use App\Events\Invoice\InvoiceWasUpdated;
use App\Helpers\Email\PaymentEmail; use App\Helpers\Email\PaymentEmail;
use App\Jobs\Payment\EmailPayment; use App\Jobs\Payment\EmailPayment;
use App\Jobs\Util\SystemLogger; use App\Jobs\Util\SystemLogger;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\SystemLog; use App\Models\SystemLog;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
class UpdateInvoicePayment class UpdateInvoicePayment
{ {
use MakesHash;
/** /**
* @deprecated This is bad logic, assumes too much. * @deprecated This is bad logic, assumes too much.
*/ */
public $payment; public $payment;
public function __construct($payment) public $payment_hash;
public function __construct($payment, $payment_hash)
{ {
$this->payment = $payment; $this->payment = $payment;
$this->payment_hash = $payment_hash;
} }
public function run() public function run()
{ {
$invoices = $this->payment->invoices()->get();
$invoices_total = $invoices->sum('balance'); $paid_invoices = $this->payment_hash->invoices();
$invoices = Invoice::whereIn('id', $this->transformKeys(array_column($paid_invoices, 'invoice_id')))->get();
/* Simplest scenario - All invoices are paid in full*/ collect($paid_invoices)->each(function ($paid_invoice) use($invoices) {
if (strval($invoices_total) === strval($this->payment->amount)) {
$invoices->each(function ($invoice) {
$this->payment
->ledger()
->updatePaymentBalance($invoice->balance*-1);
$this->payment->client
->service()
->updateBalance($invoice->balance*-1)
->updatePaidToDate($invoice->balance)
->save();
$invoice->pivot->amount = $invoice->balance;
$invoice->pivot->save();
$invoice->service() $invoice = $invoices->first(function ($inv) use($paid_invoice) {
->clearPartial() return $paid_invoice->invoice_id == $inv->hashed_id;
->updateBalance($invoice->balance*-1)
->save();
}); });
}
/*Combination of partials and full invoices are being paid*/
else {
$total = 0;
/* Calculate the grand total of the invoices*/ if($invoice->id == $this->payment_hash->fee_invoice_id)
foreach ($invoices as $invoice) { $paid_amount = $paid_invoice->amount + $this->payment_hash->fee_total;
if ($invoice->hasPartial()) { else
$total += $invoice->partial; $paid_amount = $paid_invoice->amount;
} else {
$total += $invoice->balance;
}
}
/*Test if there is a batch of partial invoices that have been paid */ $this->payment
if ($this->payment->amount == $total) { ->ledger()
$invoices->each(function ($invoice) { ->updatePaymentBalance($paid_amount*-1);
if ($invoice->hasPartial()) {
$this->payment
->ledger()
->updatePaymentBalance($invoice->partial*-1);
$this->payment->client->service() $this->payment
->updateBalance($invoice->partial*-1) ->client
->updatePaidToDate($invoice->partial) ->service()
->save(); ->updateBalance($paid_amount*-1)
->updatePaidToDate($paid_amount)
->save();
$invoice->pivot->amount = $invoice->partial; $pivot_invoice = $this->payment->invoices->first(function ($inv) use($paid_invoice){
$invoice->pivot->save(); return $inv->hashed_id == $paid_invoice->invoice_id;
});
$invoice->service()->updateBalance($invoice->partial*-1) /*update paymentable record*/
->clearPartial() $pivot_invoice->pivot->amount = $paid_amount;
->setDueDate() $pivot_invoice->save();
->setStatus(Invoice::STATUS_PARTIAL)
->save();
} else {
$this->payment
->ledger()
->updatePaymentBalance($invoice->balance*-1);
$this->payment->client->service() $invoice->service() //caution what if we amount paid was less than partial - we wipe it!
->updateBalance($invoice->balance*-1) ->clearPartial()
->updatePaidToDate($invoice->balance) ->updateBalance($paid_amount*-1)
->save(); ->save();
$invoice->pivot->amount = $invoice->balance; event(new InvoiceWasUpdated($invoice, $invoice->company, Ninja::eventVars()));
$invoice->pivot->save();
$invoice->service()->clearPartial()->updateBalance($invoice->balance*-1)->save();
}
});
} else {
SystemLogger::dispatch(
[
'payment' => $this->payment,
'invoices' => $invoices,
'invoices_total' => $invoices_total,
'payment_amount' => $this->payment->amount,
'partial_check_amount' => $total,
],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_PAYMENT_RECONCILIATION_FAILURE,
SystemLog::TYPE_LEDGER,
$this->payment->client
);
throw new \Exception("payment amount {$this->payment->amount} does not match invoice totals {$invoices_total} reversing payment"); });
$this->payment->invoice()->delete(); // } else {
$this->payment->is_deleted=true; // SystemLogger::dispatch(
$this->payment->save(); // [
$this->payment->delete(); // 'payment' => $this->payment,
} // 'invoices' => $invoices,
} // 'invoices_total' => $invoices_total,
// 'payment_amount' => $this->payment->amount,
// 'partial_check_amount' => $total,
// ],
// SystemLog::CATEGORY_GATEWAY_RESPONSE,
// SystemLog::EVENT_PAYMENT_RECONCILIATION_FAILURE,
// SystemLog::TYPE_LEDGER,
// $this->payment->client
// );
// throw new \Exception("payment amount {$this->payment->amount} does not match invoice totals {$invoices_total} reversing payment");
// $this->payment->invoice()->delete();
// $this->payment->is_deleted=true;
// $this->payment->save();
// $this->payment->delete();
// }
return $this->payment; return $this->payment;
} }

View File

@ -64,6 +64,7 @@ class CompanyGatewayTransformer extends EntityTransformer
'custom_value4' => $company_gateway->custom_value4 ?: '', 'custom_value4' => $company_gateway->custom_value4 ?: '',
'label' => (string)$company_gateway->label ?: '', 'label' => (string)$company_gateway->label ?: '',
'token_billing' => (string)$company_gateway->token_billing, 'token_billing' => (string)$company_gateway->token_billing,
'test_mode' => (bool)$company_gateway->isTestMode(),
]; ];
} }

View File

@ -33,12 +33,12 @@ class Number
/** /**
* Formats a given value based on the clients currency * Formats a given value based on the clients currency
* *
* @param float $value The number to be formatted * @param float $value The number to be formatted
* @param object $currency The client currency object * @param object $currency The client currency object
* *
* @return float The formatted value * @return string The formatted value
*/ */
public static function formatValue($value, $currency) : float public static function formatValue($value, $currency) :string
{ {
$value = floatval($value); $value = floatval($value);
@ -49,6 +49,30 @@ class Number
return number_format($value, $precision, $decimal, $thousand); return number_format($value, $precision, $decimal, $thousand);
} }
/**
* Formats a given value based on the clients currency
* BACK to a float
*
* @param string $value The formatted number to be converted back to float
* @param object $currency The client currency object
*
* @return float The formatted value
*/
public static function parseFloat($value)
{
// convert "," to "."
$s = str_replace(',', '.', $value);
// remove everything except numbers and dot "."
$s = preg_replace("/[^0-9\.]/", "", $s);
// remove all seperators from first part and keep the end
$s = str_replace('.', '',substr($s, 0, -3)) . substr($s, -3);
// return float
return (float) $s;
}
/** /**
* Formats a given value based on the clients currency AND country * Formats a given value based on the clients currency AND country
* *

View File

@ -71,8 +71,8 @@ class SystemHealth
'env_writable' => self::checkEnvWritable(), 'env_writable' => self::checkEnvWritable(),
//'mail' => self::testMailServer(), //'mail' => self::testMailServer(),
'simple_db_check' => (bool) self::simpleDbCheck(), 'simple_db_check' => (bool) self::simpleDbCheck(),
'npm_status' => self::checkNpm(), //'npm_status' => self::checkNpm(),
'node_status' => self::checkNode(), //'node_status' => self::checkNode(),
]; ];
} }

View File

@ -35,6 +35,26 @@ class AddIsPublicToDocumentsTable extends Migration
$table->softDeletes('deleted_at', 6); $table->softDeletes('deleted_at', 6);
}); });
Schema::create('payment_hashes', function ($table) {
$table->increments('id');
$table->string('hash', 255);
$table->decimal('fee_total', 16, 4);
$table->unsignedInteger('fee_invoice_id')->nullable();
$table->mediumText('data');
$table->timestamps(6);
});
Schema::table('recurring_invoices', function ($table) {
$table->string('auto_bill');
});
// Schema::table('recurring_expenses', function ($table) {
// $table->string('auto_bill');
// });
Schema::table('companies', function ($table) {
$table->enum('default_auto_bill', ['off', 'always','optin','optout'])->default('off');
});
} }

View File

@ -19,6 +19,7 @@ use App\Models\GatewayType;
use App\Models\GroupSetting; use App\Models\GroupSetting;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Payment; use App\Models\Payment;
use App\Models\PaymentHash;
use App\Models\PaymentType; use App\Models\PaymentType;
use App\Models\Quote; use App\Models\Quote;
use App\Models\User; use App\Models\User;
@ -30,6 +31,7 @@ use App\Utils\Ninja;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
class RandomDataSeeder extends Seeder class RandomDataSeeder extends Seeder
{ {
@ -213,9 +215,16 @@ class RandomDataSeeder extends Seeder
$payment->invoices()->save($invoice); $payment->invoices()->save($invoice);
$payment_hash = new PaymentHash;
$payment_hash->hash = Str::random(128);
$payment_hash->data = [['invoice_id' => $invoice->hashed_id, 'amount' => $invoice->balance]];
$payment_hash->fee_total = 0;
$payment_hash->fee_invoice_id = $invoice->id;
$payment_hash->save();
event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars())); event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars()));
$payment->service()->updateInvoicePayment(); $payment->service()->updateInvoicePayment($payment_hash);
// UpdateInvoicePayment::dispatchNow($payment, $payment->company); // UpdateInvoicePayment::dispatchNow($payment, $payment->company);
} }

View File

@ -1 +1 @@
{"assets/images/payment_types/ach.png":["assets/images/payment_types/ach.png"],"assets/images/payment_types/amex.png":["assets/images/payment_types/amex.png"],"assets/images/payment_types/carteblanche.png":["assets/images/payment_types/carteblanche.png"],"assets/images/payment_types/dinerscard.png":["assets/images/payment_types/dinerscard.png"],"assets/images/payment_types/discover.png":["assets/images/payment_types/discover.png"],"packages/font_awesome_flutter/lib/fonts/fa-brands-400.ttf":["packages/font_awesome_flutter/lib/fonts/fa-brands-400.ttf"],"packages/font_awesome_flutter/lib/fonts/fa-regular-400.ttf":["packages/font_awesome_flutter/lib/fonts/fa-regular-400.ttf"],"packages/font_awesome_flutter/lib/fonts/fa-solid-900.ttf":["packages/font_awesome_flutter/lib/fonts/fa-solid-900.ttf"],"assets/images/google-icon.png":["assets/images/google-icon.png"],"assets/images/payment_types/jcb.png":["assets/images/payment_types/jcb.png"],"assets/images/payment_types/laser.png":["assets/images/payment_types/laser.png"],"assets/images/logo.png":["assets/images/logo.png"],"assets/images/payment_types/maestro.png":["assets/images/payment_types/maestro.png"],"assets/images/payment_types/mastercard.png":["assets/images/payment_types/mastercard.png"],"packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf":["packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf"],"assets/images/payment_types/other.png":["assets/images/payment_types/other.png"],"assets/images/payment_types/paypal.png":["assets/images/payment_types/paypal.png"],"assets/images/payment_types/solo.png":["assets/images/payment_types/solo.png"],"assets/images/payment_types/switch.png":["assets/images/payment_types/switch.png"],"assets/images/payment_types/unionpay.png":["assets/images/payment_types/unionpay.png"],"assets/images/payment_types/visa.png":["assets/images/payment_types/visa.png"]} {"assets/images/payment_types/ach.png":["assets/images/payment_types/ach.png"],"assets/images/payment_types/amex.png":["assets/images/payment_types/amex.png"],"assets/images/payment_types/carteblanche.png":["assets/images/payment_types/carteblanche.png"],"assets/images/payment_types/dinerscard.png":["assets/images/payment_types/dinerscard.png"],"assets/images/payment_types/discover.png":["assets/images/payment_types/discover.png"],"assets/images/google-icon.png":["assets/images/google-icon.png"],"assets/images/payment_types/jcb.png":["assets/images/payment_types/jcb.png"],"assets/images/payment_types/laser.png":["assets/images/payment_types/laser.png"],"assets/images/logo.png":["assets/images/logo.png"],"assets/images/payment_types/maestro.png":["assets/images/payment_types/maestro.png"],"assets/images/payment_types/mastercard.png":["assets/images/payment_types/mastercard.png"],"packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf":["packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf"],"assets/images/payment_types/other.png":["assets/images/payment_types/other.png"],"assets/images/payment_types/paypal.png":["assets/images/payment_types/paypal.png"],"assets/images/payment_types/solo.png":["assets/images/payment_types/solo.png"],"assets/images/payment_types/switch.png":["assets/images/payment_types/switch.png"],"assets/images/payment_types/unionpay.png":["assets/images/payment_types/unionpay.png"],"assets/images/payment_types/visa.png":["assets/images/payment_types/visa.png"]}

View File

@ -1 +1 @@
[{"family":"MaterialIcons","fonts":[{"asset":"fonts/MaterialIcons-Regular.otf"}]},{"family":"packages/font_awesome_flutter/FontAwesomeBrands","fonts":[{"weight":400,"asset":"packages/font_awesome_flutter/lib/fonts/fa-brands-400.ttf"}]},{"family":"packages/font_awesome_flutter/FontAwesomeRegular","fonts":[{"weight":400,"asset":"packages/font_awesome_flutter/lib/fonts/fa-regular-400.ttf"}]},{"family":"packages/font_awesome_flutter/FontAwesomeSolid","fonts":[{"weight":900,"asset":"packages/font_awesome_flutter/lib/fonts/fa-solid-900.ttf"}]},{"family":"packages/material_design_icons_flutter/Material Design Icons","fonts":[{"asset":"packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf"}]}] [{"family":"MaterialIcons","fonts":[{"asset":"fonts/MaterialIcons-Regular.otf"}]},{"family":"packages/material_design_icons_flutter/Material Design Icons","fonts":[{"asset":"packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf"}]}]

View File

@ -5582,6 +5582,7 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
device_info device_info
device_info_platform_interface
google_sign_in_platform_interface google_sign_in_platform_interface
image_picker_platform_interface image_picker_platform_interface
local_auth local_auth
@ -5739,29 +5740,6 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
extended_image extended_image
MIT License
Copyright (c) 2019 zmtzawqlp
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--------------------------------------------------------------------------------
extended_image_library extended_image_library
MIT License MIT License

View File

@ -3,35 +3,32 @@ const MANIFEST = 'flutter-app-manifest';
const TEMP = 'flutter-temp-cache'; const TEMP = 'flutter-temp-cache';
const CACHE_NAME = 'flutter-app-cache'; const CACHE_NAME = 'flutter-app-cache';
const RESOURCES = { const RESOURCES = {
"main.dart.js": "3a5391955070c7845ca7187ccfba0a2f", "assets/assets/images/logo.png": "090f69e23311a4b6d851b3880ae52541",
"/": "e65799be52f7bbcaf39d78046726b95a",
"manifest.json": "77215c1737c7639764e64a192be2f7b8",
"assets/FontManifest.json": "6f5928614863ec2a06894a117283ee48",
"assets/fonts/MaterialIcons-Regular.otf": "a68d2a28c526b3b070aefca4bac93d25",
"assets/packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf": "6a2ddad1092a0a1c326b6d0e738e682b",
"assets/packages/font_awesome_flutter/lib/fonts/fa-regular-400.ttf": "2bca5ec802e40d3f4b60343e346cedde",
"assets/packages/font_awesome_flutter/lib/fonts/fa-brands-400.ttf": "5a37ae808cf9f652198acde612b5328d",
"assets/packages/font_awesome_flutter/lib/fonts/fa-solid-900.ttf": "2aa350bd2aeab88b601a593f793734c0",
"assets/AssetManifest.json": "178db3af31496d99657040f3f3434b5a",
"assets/NOTICES": "63bfe8452797d29679431def208599fb",
"assets/assets/images/logo.png": "090f69e23311a4b6d851b3880ae52541",
"assets/assets/images/payment_types/other.png": "d936e11fa3884b8c9f1bd5c914be8629",
"assets/assets/images/payment_types/switch.png": "4fa11c45327f5fdc20205821b2cfd9cc",
"assets/assets/images/payment_types/paypal.png": "8e06c094c1871376dfea1da8088c29d1",
"assets/assets/images/payment_types/discover.png": "6c0a386a00307f87db7bea366cca35f5",
"assets/assets/images/payment_types/jcb.png": "07e0942d16c5592118b72e74f2f7198c",
"assets/assets/images/payment_types/carteblanche.png": "d936e11fa3884b8c9f1bd5c914be8629",
"assets/assets/images/payment_types/mastercard.png": "6f6cdc29ee2e22e06b1ac029cb52ef71", "assets/assets/images/payment_types/mastercard.png": "6f6cdc29ee2e22e06b1ac029cb52ef71",
"assets/assets/images/payment_types/ach.png": "7433f0aff779dc98a649b7a2daf777cf",
"assets/assets/images/payment_types/solo.png": "2030c3ccaccf5d5e87916a62f5b084d6",
"assets/assets/images/payment_types/visa.png": "3ddc4a4d25c946e8ad7e6998f30fd4e3",
"assets/assets/images/payment_types/amex.png": "c49a4247984b3732a4af50a3390aa978",
"assets/assets/images/payment_types/maestro.png": "e533b92bfb50339fdbfa79e3dfe81f08",
"assets/assets/images/payment_types/unionpay.png": "7002f52004e0ab8cc0b7450b0208ccb2",
"assets/assets/images/payment_types/laser.png": "b4e6e93dd35517ac429301119ff05868", "assets/assets/images/payment_types/laser.png": "b4e6e93dd35517ac429301119ff05868",
"assets/assets/images/payment_types/jcb.png": "07e0942d16c5592118b72e74f2f7198c",
"assets/assets/images/payment_types/amex.png": "c49a4247984b3732a4af50a3390aa978",
"assets/assets/images/payment_types/paypal.png": "8e06c094c1871376dfea1da8088c29d1",
"assets/assets/images/payment_types/dinerscard.png": "06d85186ba858c18ab7c9caa42c92024", "assets/assets/images/payment_types/dinerscard.png": "06d85186ba858c18ab7c9caa42c92024",
"assets/assets/images/payment_types/ach.png": "7433f0aff779dc98a649b7a2daf777cf",
"assets/assets/images/payment_types/switch.png": "4fa11c45327f5fdc20205821b2cfd9cc",
"assets/assets/images/payment_types/discover.png": "6c0a386a00307f87db7bea366cca35f5",
"assets/assets/images/payment_types/maestro.png": "e533b92bfb50339fdbfa79e3dfe81f08",
"assets/assets/images/payment_types/carteblanche.png": "d936e11fa3884b8c9f1bd5c914be8629",
"assets/assets/images/payment_types/solo.png": "2030c3ccaccf5d5e87916a62f5b084d6",
"assets/assets/images/payment_types/unionpay.png": "7002f52004e0ab8cc0b7450b0208ccb2",
"assets/assets/images/payment_types/other.png": "d936e11fa3884b8c9f1bd5c914be8629",
"assets/assets/images/payment_types/visa.png": "3ddc4a4d25c946e8ad7e6998f30fd4e3",
"assets/assets/images/google-icon.png": "0f118259ce403274f407f5e982e681c3", "assets/assets/images/google-icon.png": "0f118259ce403274f407f5e982e681c3",
"favicon.ico": "51636d3a390451561744c42188ccd628" "assets/NOTICES": "2ea764e7f73033bab6d857b898e4a688",
"assets/AssetManifest.json": "ea09ed4b9b8b6c83d6896248aac7c527",
"assets/packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf": "6a2ddad1092a0a1c326b6d0e738e682b",
"assets/FontManifest.json": "cf3c681641169319e61b61bd0277378f",
"assets/fonts/MaterialIcons-Regular.otf": "a68d2a28c526b3b070aefca4bac93d25",
"favicon.ico": "51636d3a390451561744c42188ccd628",
"main.dart.js": "40d8530505b04054904b6f285d266e65",
"/": "e65799be52f7bbcaf39d78046726b95a",
"manifest.json": "77215c1737c7639764e64a192be2f7b8"
}; };
// The application shell files that are downloaded before a service worker can // The application shell files that are downloaded before a service worker can

237892
public/main.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -11,6 +11,11 @@
<style> <style>
/* fix for blurry fonts */
flt-glass-pane {
image-rendering: pixelated;
}
/* https://projects.lukehaas.me/css-loaders/ */ /* https://projects.lukehaas.me/css-loaders/ */
.loader, .loader,
.loader:before, .loader:before,

View File

@ -11,7 +11,7 @@
{!! Former::hidden('gateway_response')->id('gateway_response') !!} {!! Former::hidden('gateway_response')->id('gateway_response') !!}
{!! Former::hidden('store_card')->id('store_card') !!} {!! Former::hidden('store_card')->id('store_card') !!}
{!! Former::hidden('hashed_ids')->value($hashed_ids) !!} {!! Former::hidden('payment_hash')->value($payment_hash) !!}
{!! Former::hidden('company_gateway_id')->value($payment_method_id) !!} {!! Former::hidden('company_gateway_id')->value($payment_method_id) !!}
{!! Former::hidden('payment_method_id')->value($gateway->getCompanyGatewayId()) !!} {!! Former::hidden('payment_method_id')->value($gateway->getCompanyGatewayId()) !!}
{!! Former::close() !!} {!! Former::close() !!}

View File

@ -1,8 +1,8 @@
<div> <div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center"> <div class="flex items-center">
<span class="mr-2 text-sm hidden md:block">{{ ctrans('texts.per_page') }}</span> <span class="hidden mr-2 text-sm md:block">{{ ctrans('texts.per_page') }}</span>
<select wire:model="per_page" class="form-select py-1 text-sm"> <select wire:model="per_page" class="py-1 text-sm form-select">
<option>5</option> <option>5</option>
<option selected>10</option> <option selected>10</option>
<option>15</option> <option>15</option>
@ -11,50 +11,54 @@
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<div class="mr-3"> <div class="mr-3">
<input wire:click="statusChange('paid')" type="checkbox" class="form-checkbox cursor-pointer" id="paid-checkbox"> <input wire:model="status" value="paid" type="checkbox" class="cursor-pointer form-checkbox" id="paid-checkbox">
<label for="paid-checkbox" class="text-sm cursor-pointer">{{ ctrans('texts.status_paid') }}</label> <label for="paid-checkbox" class="text-sm cursor-pointer">{{ ctrans('texts.status_paid') }}</label>
</div> </div>
<div class="mr-3"> <div class="mr-3">
<input wire:click="statusChange('unpaid')" type="checkbox" class="form-checkbox cursor-pointer" id="unpaid-checkbox"> <input wire:model="status" value="unpaid" type="checkbox" class="cursor-pointer form-checkbox" id="unpaid-checkbox">
<label for="unpaid-checkbox" class="text-sm cursor-pointer">{{ ctrans('texts.status_unpaid') }}</label> <label for="unpaid-checkbox" class="text-sm cursor-pointer">{{ ctrans('texts.status_unpaid') }}</label>
</div> </div>
<div class="mr-3"> <div class="mr-3">
<input wire:click="statusChange('overdue')" type="checkbox" class="form-checkbox cursor-pointer" id="overdue-checkbox"> <input wire:model="status" value="overdue" type="checkbox" class="cursor-pointer form-checkbox" id="overdue-checkbox">
<label for="overdue-checkbox" class="text-sm cursor-pointer">{{ ctrans('texts.overdue') }}</label> <label for="overdue-checkbox" class="text-sm cursor-pointer">{{ ctrans('texts.overdue') }}</label>
</div> </div>
<div class="mr-3">
<input wire:model="status" value="gateway_fees" type="checkbox" class="cursor-pointer form-checkbox" id="gateway-fees-checkbox">
<label for="gateway-fees-checkbox" class="text-sm cursor-pointer">{{ ctrans('texts.gateway_fees') }}</label>
</div>
</div> </div>
</div> </div>
<div class="-my-2 py-2 overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8"> <div class="py-2 -my-2 overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
<div class="align-middle inline-block min-w-full overflow-hidden rounded"> <div class="inline-block min-w-full overflow-hidden align-middle rounded">
<table class="min-w-full shadow rounded border border-gray-200 mt-4 invoices-table"> <table class="min-w-full mt-4 border border-gray-200 rounded shadow invoices-table">
<thead> <thead>
<tr> <tr>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider"> <th class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-500 uppercase border-b border-gray-200 bg-gray-50">
<label> <label>
<input type="checkbox" class="form-check form-check-parent"> <input type="checkbox" class="form-check form-check-parent">
</label> </label>
</th> </th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider"> <th class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-500 uppercase border-b border-gray-200 bg-gray-50">
<span role="button" wire:click="sortBy('number')" class="cursor-pointer"> <span role="button" wire:click="sortBy('number')" class="cursor-pointer">
{{ ctrans('texts.invoice_number') }} {{ ctrans('texts.invoice_number') }}
</span> </span>
</th> </th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider"> <th class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-500 uppercase border-b border-gray-200 bg-gray-50">
<span role="button" wire:click="sortBy('date')" class="cursor-pointer"> <span role="button" wire:click="sortBy('date')" class="cursor-pointer">
{{ ctrans('texts.invoice_date') }} {{ ctrans('texts.invoice_date') }}
</span> </span>
</th> </th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider"> <th class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-500 uppercase border-b border-gray-200 bg-gray-50">
<span role="button" wire:click="sortBy('balance')" class="cursor-pointer"> <span role="button" wire:click="sortBy('balance')" class="cursor-pointer">
{{ ctrans('texts.balance') }} {{ ctrans('texts.balance') }}
</span> </span>
</th> </th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider"> <th class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-500 uppercase border-b border-gray-200 bg-gray-50">
<span role="button" wire:click="sortBy('due_date')" class="cursor-pointer"> <span role="button" wire:click="sortBy('due_date')" class="cursor-pointer">
{{ ctrans('texts.due_date') }} {{ ctrans('texts.due_date') }}
</span> </span>
</th> </th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider"> <th class="px-6 py-3 text-xs font-medium leading-4 tracking-wider text-left text-gray-500 uppercase border-b border-gray-200 bg-gray-50">
<span role="button" wire:click="sortBy('status_id')" class="cursor-pointer"> <span role="button" wire:click="sortBy('status_id')" class="cursor-pointer">
{{ ctrans('texts.status') }} {{ ctrans('texts.status') }}
</span> </span>
@ -65,33 +69,33 @@
<tbody> <tbody>
@forelse($invoices as $invoice) @forelse($invoices as $invoice)
<tr class="bg-white group hover:bg-gray-100"> <tr class="bg-white group hover:bg-gray-100">
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 font-medium text-gray-900"> <td class="px-6 py-4 text-sm font-medium leading-5 text-gray-900 whitespace-no-wrap">
<label> <label>
<input type="checkbox" class="form-check form-check-child" data-value="{{ $invoice->hashed_id }}"> <input type="checkbox" class="form-check form-check-child" data-value="{{ $invoice->hashed_id }}">
</label> </label>
</td> </td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500"> <td class="px-6 py-4 text-sm leading-5 text-gray-500 whitespace-no-wrap">
{{ $invoice->number }} {{ $invoice->number }}
</td> </td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500"> <td class="px-6 py-4 text-sm leading-5 text-gray-500 whitespace-no-wrap">
{{ $invoice->formatDate($invoice->date, $invoice->client->date_format()) }} {{ $invoice->formatDate($invoice->date, $invoice->client->date_format()) }}
</td> </td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500"> <td class="px-6 py-4 text-sm leading-5 text-gray-500 whitespace-no-wrap">
{{ App\Utils\Number::formatMoney($invoice->balance, $invoice->client) }} {{ App\Utils\Number::formatMoney($invoice->balance, $invoice->client) }}
</td> </td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500"> <td class="px-6 py-4 text-sm leading-5 text-gray-500 whitespace-no-wrap">
{{ $invoice->formatDate($invoice->due_date, $invoice->client->date_format()) }} {{ $invoice->formatDate($invoice->due_date, $invoice->client->date_format()) }}
</td> </td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500"> <td class="px-6 py-4 text-sm leading-5 text-gray-500 whitespace-no-wrap">
{!! App\Models\Invoice::badgeForStatus($invoice->status) !!} {!! App\Models\Invoice::badgeForStatus($invoice->status) !!}
</td> </td>
<td class="px-6 py-4 whitespace-no-wrap flex items-center justify-end text-sm leading-5 font-medium"> <td class="flex items-center justify-end px-6 py-4 text-sm font-medium leading-5 whitespace-no-wrap">
@if($invoice->isPayable()) @if($invoice->isPayable())
<form action="{{ route('client.invoices.bulk') }}" method="post"> <form action="{{ route('client.invoices.bulk') }}" method="post">
@csrf @csrf
<input type="hidden" name="invoices[]" value="{{ $invoice->hashed_id }}"> <input type="hidden" name="invoices[]" value="{{ $invoice->hashed_id }}">
<input type="hidden" name="action" value="payment"> <input type="hidden" name="action" value="payment">
<button class="button button-primary py-1 px-2 text-xs uppercase mr-3"> <button class="px-2 py-1 mr-3 text-xs uppercase button button-primary">
@lang('texts.pay_now') @lang('texts.pay_now')
</button> </button>
</form> </form>
@ -103,7 +107,7 @@
</tr> </tr>
@empty @empty
<tr class="bg-white group hover:bg-gray-100"> <tr class="bg-white group hover:bg-gray-100">
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500" colspan="100%"> <td class="px-6 py-4 text-sm leading-5 text-gray-500 whitespace-no-wrap" colspan="100%">
{{ ctrans('texts.no_results') }} {{ ctrans('texts.no_results') }}
</td> </td>
</tr> </tr>
@ -112,9 +116,9 @@
</table> </table>
</div> </div>
</div> </div>
<div class="flex justify-center md:justify-between mt-6 mb-6"> <div class="flex justify-center mt-6 mb-6 md:justify-between">
@if($invoices->total() > 0) @if($invoices->total() > 0)
<span class="text-gray-700 text-sm hidden md:block"> <span class="hidden text-sm text-gray-700 md:block">
{{ ctrans('texts.showing_x_of', ['first' => $invoices->firstItem(), 'last' => $invoices->lastItem(), 'total' => $invoices->total()]) }} {{ ctrans('texts.showing_x_of', ['first' => $invoices->firstItem(), 'last' => $invoices->lastItem(), 'total' => $invoices->total()]) }}
</span> </span>
@endif @endif

View File

@ -11,19 +11,19 @@
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<div class="mr-3"> <div class="mr-3">
<input wire:click="statusChange('draft')" type="checkbox" class="cursor-pointer form-checkbox" id="draft-checkbox"> <input wire:model="status" value="{{ App\Models\Quote::STATUS_DRAFT }}" type="checkbox" class="cursor-pointer form-checkbox" id="draft-checkbox">
<label for="draft-checkbox" class="text-sm cursor-pointer">{{ ctrans('texts.status_draft') }}</label> <label for="draft-checkbox" class="text-sm cursor-pointer">{{ ctrans('texts.status_draft') }}</label>
</div> </div>
<div class="mr-3"> <div class="mr-3">
<input wire:click="statusChange('sent')" value="sent" type="checkbox" class="cursor-pointer form-checkbox" id="sent-checkbox"> <input wire:model="status" value="{{ App\Models\Quote::STATUS_SENT }}" value="sent" type="checkbox" class="cursor-pointer form-checkbox" id="sent-checkbox">
<label for="sent-checkbox" class="text-sm cursor-pointer">{{ ctrans('texts.status_pending') }}</label> <label for="sent-checkbox" class="text-sm cursor-pointer">{{ ctrans('texts.status_pending') }}</label>
</div> </div>
<div class="mr-3"> <div class="mr-3">
<input wire:click="statusChange('approved')" value="approved" type="checkbox" class="cursor-pointer form-checkbox" id="approved-checkbox"> <input wire:model="status" value="{{ App\Models\Quote::STATUS_APPROVED }}" value="approved" type="checkbox" class="cursor-pointer form-checkbox" id="approved-checkbox">
<label for="approved-checkbox" class="text-sm cursor-pointer">{{ ctrans('texts.approved') }}</label> <label for="approved-checkbox" class="text-sm cursor-pointer">{{ ctrans('texts.approved') }}</label>
</div> </div>
<div class="mr-3"> <div class="mr-3">
<input wire:click="statusChange('expired')" value="expired" type="checkbox" class="cursor-pointer form-checkbox" id="expired-checkbox"> <input wire:model="status" value="{{ App\Models\Quote::STATUS_EXPIRED }}" value="expired" type="checkbox" class="cursor-pointer form-checkbox" id="expired-checkbox">
<label for="expired-checkbox" class="text-sm cursor-pointer">{{ ctrans('texts.expired') }}</label> <label for="expired-checkbox" class="text-sm cursor-pointer">{{ ctrans('texts.expired') }}</label>
</div> </div>
</div> </div>

View File

@ -13,9 +13,7 @@
@section('body') @section('body')
<form action="{{ route('client.payments.response') }}" method="post" id="server_response"> <form action="{{ route('client.payments.response') }}" method="post" id="server_response">
@csrf @csrf
@foreach($invoices as $invoice) <input type="hidden" name="payment_hash" value="{{ $payment_hash }}">
<input type="hidden" name="hashed_ids[]" value="{{ $invoice->hashed_id }}">
@endforeach
<input type="hidden" name="company_gateway_id" value="{{ $gateway->id }}"> <input type="hidden" name="company_gateway_id" value="{{ $gateway->id }}">
<input type="hidden" name="payment_method_id" value="1"> <input type="hidden" name="payment_method_id" value="1">
<input type="hidden" name="gateway_response" id="gateway_response"> <input type="hidden" name="gateway_response" id="gateway_response">
@ -23,7 +21,7 @@
<input type="hidden" name="dataDescriptor" id="dataDescriptor" /> <input type="hidden" name="dataDescriptor" id="dataDescriptor" />
<input type="hidden" name="token" id="token" /> <input type="hidden" name="token" id="token" />
<input type="hidden" name="store_card" id="store_card" /> <input type="hidden" name="store_card" id="store_card" />
<input type="hidden" name="amount_with_fee" id="amount_with_fee" value="{{ $amount_with_fee }}" /> <input type="hidden" name="amount_with_fee" id="amount_with_fee" value="{{ $total['amount_with_fee'] }}" />
</form> </form>
<div class="container mx-auto"> <div class="container mx-auto">
<div class="grid grid-cols-6 gap-4"> <div class="grid grid-cols-6 gap-4">
@ -40,10 +38,22 @@
<dl> <dl>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"> <div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500"> <dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.amount') }} {{ ctrans('texts.subtotal') }}
</dt> </dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2"> <dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ App\Utils\Number::formatMoney($amount_with_fee, $client) }} {{ App\Utils\Number::formatMoney($total['invoice_totals'], $client) }}
</dd>
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.gateway_fees') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ App\Utils\Number::formatMoney($total['fee_total'], $client) }}
</dd>
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.total') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ App\Utils\Number::formatMoney($total['amount_with_fee'], $client) }}
</dd> </dd>
</div> </div>
@ -67,11 +77,23 @@
<div> <div>
<dl> <dl>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"> <div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.totals') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ App\Utils\Number::formatMoney($total['invoice_totals'], $client) }}
</dd>
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.gateway_fees') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ App\Utils\Number::formatMoney($total['fee_total'], $client) }}
</dd>
<dt class="text-sm leading-5 font-medium text-gray-500"> <dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.amount') }} {{ ctrans('texts.amount') }}
</dt> </dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2"> <dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ App\Utils\Number::formatMoney($amount_with_fee, $client) }} {{ App\Utils\Number::formatMoney($total['amount_with_fee'], $client) }}
</dd> </dd>
</div> </div>
@foreach($tokens as $token) @foreach($tokens as $token)

View File

@ -11,9 +11,8 @@
@csrf @csrf
<input type="hidden" name="gateway_response"> <input type="hidden" name="gateway_response">
<input type="hidden" name="store_card"> <input type="hidden" name="store_card">
@foreach($invoices as $invoice) <input type="hidden" name="payment_hash" value="{{$payment_hash}}">
<input type="hidden" name="hashed_ids[]" value="{{ $invoice->hashed_id }}">
@endforeach
<input type="hidden" name="company_gateway_id" value="{{ $gateway->getCompanyGatewayId() }}"> <input type="hidden" name="company_gateway_id" value="{{ $gateway->getCompanyGatewayId() }}">
<input type="hidden" name="payment_method_id" value="{{ $payment_method_id }}"> <input type="hidden" name="payment_method_id" value="{{ $payment_method_id }}">
</form> </form>

View File

@ -10,13 +10,10 @@
@section('body') @section('body')
<form action="{{ route('client.payments.process') }}" method="post" id="payment-form"> <form action="{{ route('client.payments.process') }}" method="post" id="payment-form">
@csrf @csrf
@foreach($invoices as $invoice)
<input type="hidden" name="invoices[]" value="{{ $invoice->hashed_id }}">
@endforeach
<input type="hidden" name="company_gateway_id" id="company_gateway_id"> <input type="hidden" name="company_gateway_id" id="company_gateway_id">
<input type="hidden" name="payment_method_id" id="payment_method_id"> <input type="hidden" name="payment_method_id" id="payment_method_id">
<input type="hidden" name="signature"> <input type="hidden" name="signature">
</form>
<div class="container mx-auto"> <div class="container mx-auto">
<div class="grid grid-cols-6 gap-4"> <div class="grid grid-cols-6 gap-4">
<div class="col-span-6 md:col-start-2 md:col-span-4"> <div class="col-span-6 md:col-start-2 md:col-span-4">
@ -63,7 +60,8 @@
</div> </div>
</div> </div>
@foreach($invoices as $invoice) @foreach($invoices as $key => $invoice)
<input type="hidden" name="payable_invoices[{{$key}}][invoice_id]" value="{{ $invoice->hashed_id }}">
<div class="bg-white shadow overflow-hidden sm:rounded-lg mb-4"> <div class="bg-white shadow overflow-hidden sm:rounded-lg mb-4">
<div class="px-4 py-5 border-b border-gray-200 sm:px-6"> <div class="px-4 py-5 border-b border-gray-200 sm:px-6">
<h3 class="text-lg leading-6 font-medium text-gray-900"> <h3 class="text-lg leading-6 font-medium text-gray-900">
@ -104,7 +102,7 @@
@elseif($invoice->public_notes) @elseif($invoice->public_notes)
{{ $invoice->public_notes }} {{ $invoice->public_notes }}
@else @else
{{ $invoice->invoice_date}} {{ $invoice->date}}
@endif @endif
</dd> </dd>
</div> </div>
@ -113,7 +111,8 @@
{{ ctrans('texts.amount') }} {{ ctrans('texts.amount') }}
</dt> </dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2"> <dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ App\Utils\Number::formatMoney($invoice->amount, $invoice->client) }} <!-- App\Utils\Number::formatMoney($invoice->amount, $invoice->client) -->
<input type="text" name="payable_invoices[{{$key}}][amount]" value="{{ $invoice->partial > 0 ? $invoice->partial : $invoice->balance }}">
</dd> </dd>
</div> </div>
</dl> </dl>
@ -123,7 +122,7 @@
</div> </div>
</div> </div>
</div> </div>
</form>
@include('portal.ninja2020.invoices.includes.terms') @include('portal.ninja2020.invoices.includes.terms')
@include('portal.ninja2020.invoices.includes.signature') @include('portal.ninja2020.invoices.includes.signature')
@endsection @endsection

View File

@ -34,13 +34,13 @@
@include('setup._issues') @include('setup._issues')
@else @else
@if(!$check['npm_status']) @if(isset($check['npm_status']) && !$check['npm_status'])
<div class="alert alert-success mt-4"> <div class="alert alert-success mt-4">
<p>NPM Version => {{$check['npm_status']}}</p> <p>NPM Version => {{$check['npm_status']}}</p>
</div> </div>
@endif @endif
@if(!$check['node_status']) @if(isset($check['node_status']) && !$check['node_status'])
<div class="alert alert-success mt-4"> <div class="alert alert-success mt-4">
<p>Node Version => {{$check['node_status']}}</p> <p>Node Version => {{$check['node_status']}}</p>
</div> </div>

View File

@ -314,7 +314,41 @@ class CompanyGatewayApiTest extends TestCase
$company_gateway = CompanyGateway::find($id); $company_gateway = CompanyGateway::find($id);
$this->assertEquals(11, $company_gateway->calcGatewayFee(10)); $this->assertEquals(11, $company_gateway->calcGatewayFee(10, true));
}
public function testFeesAndLimitsFeePercentAndAmountAndTaxCalcuationInclusiveTaxes()
{
//{"1":{"min_limit":1,"max_limit":1000000,"fee_amount":10,"fee_percent":2,"fee_tax_name1":"","fee_tax_name2":"","fee_tax_name3":"","fee_tax_rate1":0,"fee_tax_rate2":0,"fee_tax_rate3":0,"fee_cap":10,"adjust_fee_percent":true}}
$fee = new FeesAndLimits;
$fee->fee_amount = 10;
// $fee->fee_percent = 2;
$fee->fee_tax_name1 = 'GST';
$fee->fee_tax_rate1 = '10.0';
$fee_arr[1] = (array)$fee;
$data = [
'config' => 'random config',
'gateway_key' => '3b6621f970ab18887c4f6dca78d3f8bb',
'fees_and_limits' => $fee_arr,
];
/* POST */
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token
])->post('/api/v1/company_gateways', $data);
$response->assertStatus(200);
$arr = $response->json();
$id = $this->decodePrimaryKey($arr['data']['id']);
$company_gateway = CompanyGateway::find($id);
$this->assertEquals(10, $company_gateway->calcGatewayFee(10));
} }
public function testFeesAndLimitsFeePercentAndAmountAndDoubleTaxCalcuation() public function testFeesAndLimitsFeePercentAndAmountAndDoubleTaxCalcuation()
@ -351,7 +385,7 @@ class CompanyGatewayApiTest extends TestCase
$company_gateway = CompanyGateway::find($id); $company_gateway = CompanyGateway::find($id);
$this->assertEquals(12, $company_gateway->calcGatewayFee(10)); $this->assertEquals(12, $company_gateway->calcGatewayFee(10,true));
} }
@ -389,6 +423,6 @@ class CompanyGatewayApiTest extends TestCase
$company_gateway = CompanyGateway::find($id); $company_gateway = CompanyGateway::find($id);
$this->assertEquals(1, $company_gateway->calcGatewayFee(10)); $this->assertEquals(1.2, $company_gateway->calcGatewayFee(10,true));
} }
} }

View File

@ -49,6 +49,7 @@ class CompanyGatewayTest extends TestCase
$data[1]['fee_tax_rate2'] = ''; $data[1]['fee_tax_rate2'] = '';
$data[1]['fee_tax_name3'] = ''; $data[1]['fee_tax_name3'] = '';
$data[1]['fee_tax_rate3'] = 0; $data[1]['fee_tax_rate3'] = 0;
$data[1]['fee_cap'] = 0;
$cg = new CompanyGateway; $cg = new CompanyGateway;
$cg->company_id = $this->company->id; $cg->company_id = $this->company->id;
@ -107,4 +108,125 @@ class CompanyGatewayTest extends TestCase
return $passes; return $passes;
} }
public function testFeesAreAppendedToInvoice() //after refactor this may be redundant
{
$data = [];
$data[1]['min_limit'] = -1;
$data[1]['max_limit'] = -1;
$data[1]['fee_amount'] = 1.00;
$data[1]['fee_percent'] = 0.000;
$data[1]['fee_tax_name1'] = '';
$data[1]['fee_tax_rate1'] = 0;
$data[1]['fee_tax_name2'] = '';
$data[1]['fee_tax_rate2'] = 0;
$data[1]['fee_tax_name3'] = '';
$data[1]['fee_tax_rate3'] = 0;
$data[1]['fee_cap'] = 0;
$cg = new CompanyGateway;
$cg->company_id = $this->company->id;
$cg->user_id = $this->user->id;
$cg->gateway_key = 'd14dd26a37cecc30fdd65700bfb55b23';
$cg->require_cvv = true;
$cg->show_billing_address = true;
$cg->show_shipping_address = true;
$cg->update_details = true;
$cg->config = encrypt(config('ninja.testvars.stripe'));
$cg->fees_and_limits = $data;
$cg->save();
$balance = $this->invoice->balance;
$this->invoice = $this->invoice->service()->addGatewayFee($cg, $this->invoice->balance)->save();
$this->invoice = $this->invoice->calc()->getInvoice();
$items = $this->invoice->line_items;
$this->assertEquals(($balance+1), $this->invoice->balance);
}
public function testProRataGatewayFees()
{
$data = [];
$data[1]['min_limit'] = -1;
$data[1]['max_limit'] = -1;
$data[1]['fee_amount'] = 1.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;
$cg = new CompanyGateway;
$cg->company_id = $this->company->id;
$cg->user_id = $this->user->id;
$cg->gateway_key = 'd14dd26a37cecc30fdd65700bfb55b23';
$cg->require_cvv = true;
$cg->show_billing_address = true;
$cg->show_shipping_address = true;
$cg->update_details = true;
$cg->config = encrypt(config('ninja.testvars.stripe'));
$cg->fees_and_limits = $data;
$cg->save();
$total = 10.93;
$total_invoice_count = 5;
$total_gateway_fee = round($cg->calcGatewayFee($total,true),2);
$this->assertEquals(1.58, $total_gateway_fee);
/*simple pro rata*/
$fees_and_limits = $cg->getFeesAndLimits();
/*Calculate all subcomponents of the fee*/
// $fee_component_amount = $fees_and_limits->fee_amount ?: 0;
// $fee_component_percent = $fees_and_limits->fee_percent ? ($total * $fees_and_limits->fee_percent / 100) : 0;
// $combined_fee_component = $fee_component_amount + $fee_component_percent;
// $fee_component_tax_name1 = $fees_and_limits->fee_tax_name1 ?: '';
// $fee_component_tax_rate1 = $fees_and_limits->fee_tax_rate1 ? ($combined_fee_component * $fees_and_limits->fee_tax_rate1 / 100) : 0;
// $fee_component_tax_name2 = $fees_and_limits->fee_tax_name2 ?: '';
// $fee_component_tax_rate2 = $fees_and_limits->fee_tax_rate2 ? ($combined_fee_component * $fees_and_limits->fee_tax_rate2 / 100) : 0;
// $fee_component_tax_name3 = $fees_and_limits->fee_tax_name3 ?: '';
// $fee_component_tax_rate3 = $fees_and_limits->fee_tax_rate3 ? ($combined_fee_component * $fees_and_limits->fee_tax_rate3 / 100) : 0;
// $pro_rata_fee = round($total_gateway_fee / $total_invoice_count,2);
// while($pro_rata_fee * $total_invoice_count != $total_gateway_fee) {
// //nudge one pro rata fee until we get the desired amount
// $sub_total_fees = ($pro_rata_fee*($total_invoice_count--));
// //work out if we have to nudge up or down
// if($pro_rata_fee*$total_invoice_count > $total_gateway_fee) {
// //nudge DOWN
// $pro_rata_fee - 0.01; //this will break if the currency doesn't have decimals
// }
// else {
// //nudge UP
// }
// }
// $this->assertEquals(1.56, $pro_rata_fee*$total_invoice_count);
}
} }

View File

@ -50,9 +50,9 @@ class ExampleIntegrationTest extends TestCase
->design($design) ->design($design)
->build(); ->build();
exec('echo "" > storage/logs/laravel.log'); // exec('echo "" > storage/logs/laravel.log');
info($maker->getCompiledHTML(true)); // info($maker->getCompiledHTML(true));
$this->assertTrue(true); $this->assertTrue(true);
} }

View File

@ -360,9 +360,9 @@ class PdfMakerTest extends TestCase
->design($design) ->design($design)
->build(); ->build();
exec('echo "" > storage/logs/laravel.log'); // exec('echo "" > storage/logs/laravel.log');
info($maker->getCompiledHTML(true)); // info($maker->getCompiledHTML(true));
$this->assertTrue(true); $this->assertTrue(true);
} }

View File

@ -226,7 +226,7 @@ class CompanyLedgerTest extends TestCase
$payment_ledger = $payment->company_ledger->sortByDesc('id')->first(); $payment_ledger = $payment->company_ledger->sortByDesc('id')->first();
info($payment->client->balance); //info($payment->client->balance);
$this->assertEquals($payment->client->balance, $payment_ledger->balance); $this->assertEquals($payment->client->balance, $payment_ledger->balance);
$this->assertEquals($payment->client->paid_to_date, 10); $this->assertEquals($payment->client->paid_to_date, 10);

View File

@ -2,6 +2,7 @@
namespace Tests\Unit; namespace Tests\Unit;
use App\Models\Currency;
use App\Utils\Number; use App\Utils\Number;
use Tests\TestCase; use Tests\TestCase;
@ -31,4 +32,25 @@ class NumberTest extends TestCase
$this->assertEquals(2.15, $rounded); $this->assertEquals(2.15, $rounded);
} }
public function testParsingFloats()
{
Currency::all()->each(function ($currency){
$amount = 123456789.12;
$formatted_amount = Number::formatValue($amount, $currency);
$float_amount = Number::parseFloat($formatted_amount);
if($currency->precision == 0){
$this->assertEquals(123456789, $float_amount);
}
else
$this->assertEquals($amount, $float_amount);
});
}
} }