From 979916adb56c0e9c6316a19716d2898fec141ddd Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 25 Aug 2020 23:06:38 +1000 Subject: [PATCH 01/20] Refactor payments --- .../ClientPortal/InvoiceController.php | 2 + .../ClientPortal/PaymentController.php | 49 ++++++++++++-- app/Models/PaymentHash.php | 28 ++++++++ app/Services/Invoice/AddGatewayFee.php | 67 +++++++++++++++++++ app/Services/Invoice/InvoiceService.php | 7 ++ ...40557_add_is_public_to_documents_table.php | 6 ++ 6 files changed, 155 insertions(+), 4 deletions(-) create mode 100644 app/Models/PaymentHash.php create mode 100644 app/Services/Invoice/AddGatewayFee.php diff --git a/app/Http/Controllers/ClientPortal/InvoiceController.php b/app/Http/Controllers/ClientPortal/InvoiceController.php index fe8d6adf2a1e..4269038b3909 100644 --- a/app/Http/Controllers/ClientPortal/InvoiceController.php +++ b/app/Http/Controllers/ClientPortal/InvoiceController.php @@ -113,6 +113,8 @@ class InvoiceController extends Controller 'total' => $total, ]; + //REFACTOR entry point for online payments starts here + return $this->render('invoices.payment', $data); } diff --git a/app/Http/Controllers/ClientPortal/PaymentController.php b/app/Http/Controllers/ClientPortal/PaymentController.php index d5cf93add9b0..ce8d9dcd211c 100644 --- a/app/Http/Controllers/ClientPortal/PaymentController.php +++ b/app/Http/Controllers/ClientPortal/PaymentController.php @@ -72,11 +72,29 @@ class PaymentController extends Controller */ public function process() { - $invoices = Invoice::whereIn('id', $this->transformKeys(request()->invoices)) - ->where('company_id', auth('contact')->user()->company->id) - ->get(); + //REFACTOR - Here the request will contain an array of invoices and the amount to be charged for the invoice + //REFACTOR - At this point, we will also need to modify the invoice to include a line item for a gateway fee if applicable + // 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. + // [ + // 'invoices' => + // [ + // 'invoice_id' => 'xx', + // 'amount' => 'yy', + // ] + // ] + + //old + // $invoices = Invoice::whereIn('id', $this->transformKeys(request()->invoices)) + // ->where('company_id', auth('contact')->user()->company->id) + // ->get(); - $amount = $invoices->sum('balance'); + + $invoices = Invoice::whereIn('id', $this->transformKeys(array_column($request()->invoices, 'invoice_id')))->get(); + + //old + // $amount = $invoices->sum('balance'); $invoices = $invoices->filter(function ($invoice) { return $invoice->isPayable(); @@ -88,6 +106,14 @@ class PaymentController extends Controller ->with(['warning' => 'No payable invoices selected.']); } + foreach(request()->invoices as $payable_invoice) + { + $invoice = Invoice::find($this->decodePrimaryKey($payable_invoice['invoice_id'])); + + $invoice->service()->addGatewayFee($payable_invoice['amount']); + } + + $invoices->map(function ($invoice) { $invoice->balance = Number::formatMoney($invoice->balance, $invoice->client); $invoice->due_date = $this->formatDate($invoice->due_date, $invoice->client->date_format()); @@ -127,6 +153,21 @@ class PaymentController extends Controller { $gateway = CompanyGateway::find($request->input('company_gateway_id')); + //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 ->driver(auth()->user()->client) ->setPaymentMethod($request->input('payment_method_id')) diff --git a/app/Models/PaymentHash.php b/app/Models/PaymentHash.php new file mode 100644 index 000000000000..d9c2c2541052 --- /dev/null +++ b/app/Models/PaymentHash.php @@ -0,0 +1,28 @@ + 'object' + ]; + + + public function invoices() + { + return $this->data->invoices; + } +} diff --git a/app/Services/Invoice/AddGatewayFee.php b/app/Services/Invoice/AddGatewayFee.php new file mode 100644 index 000000000000..f6e8891d9b38 --- /dev/null +++ b/app/Services/Invoice/AddGatewayFee.php @@ -0,0 +1,67 @@ +company_gateway = $company_gateway; + + $this->invoice = $invoice; + + $this->amount = $amount; + } + + public function run() + { + $gateway_fee = $this->company_gateway->calcGatewayFee($this->amount); + + if($gateway_fee > 0) + return $this->processGatewayFee($gateway_fee); + + return $this->processGatewayDiscount($gateway_fee); + + + } + + private function processGatewayFee($gateway_fee) + { + $invoice_item = new InvoiceItem; + $invoice_item->type_id = 3; + $invoice_item->notes = ctrans('texts.Gateway Fee Surcharge'); + } + + private function processGatewayDiscount($gateway_fee) + { + + } +} diff --git a/app/Services/Invoice/InvoiceService.php b/app/Services/Invoice/InvoiceService.php index 714b2727ea05..ef36b58cb37c 100644 --- a/app/Services/Invoice/InvoiceService.php +++ b/app/Services/Invoice/InvoiceService.php @@ -11,6 +11,7 @@ namespace App\Services\Invoice; +use App\Models\CompanyGateway; use App\Models\Invoice; use App\Models\Payment; use App\Services\Client\ClientService; @@ -76,6 +77,12 @@ class InvoiceService return $this; } + public function addGatewayFee(CompanyGateway $company_gateway, float $amount) + { + $this->invoice = (new AddGatewayFee($company_gateway, $this->invoice, $amoun))->run(); + + return $this; + } /** * Update an invoice balance * @param float $balance_adjustment The amount to adjust the invoice by diff --git a/database/migrations/2020_08_18_140557_add_is_public_to_documents_table.php b/database/migrations/2020_08_18_140557_add_is_public_to_documents_table.php index 784db3ae356f..92c6b9627c22 100644 --- a/database/migrations/2020_08_18_140557_add_is_public_to_documents_table.php +++ b/database/migrations/2020_08_18_140557_add_is_public_to_documents_table.php @@ -35,6 +35,12 @@ class AddIsPublicToDocumentsTable extends Migration $table->softDeletes('deleted_at', 6); }); + Schema::create('payment_hashes', function ($table) { + $table->increments('id'); + $table->string('hash', 255); + $table->mediumText('data'); + $table->timestamps(6); + }); } From 1c55b529a0b367bca9d09f3429e88e376de2b9af Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 25 Aug 2020 23:18:17 +1000 Subject: [PATCH 02/20] Refactor payments --- .../ClientPortal/PaymentController.php | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/app/Http/Controllers/ClientPortal/PaymentController.php b/app/Http/Controllers/ClientPortal/PaymentController.php index ce8d9dcd211c..1b1f4ac70f39 100644 --- a/app/Http/Controllers/ClientPortal/PaymentController.php +++ b/app/Http/Controllers/ClientPortal/PaymentController.php @@ -90,31 +90,38 @@ class PaymentController extends Controller // ->where('company_id', auth('contact')->user()->company->id) // ->get(); - - $invoices = Invoice::whereIn('id', $this->transformKeys(array_column($request()->invoices, 'invoice_id')))->get(); + /*find invoices*/ + $invoices = Invoice::whereIn('id', $this->transformKeys(array_column(request()->invoices, 'invoice_id')))->get(); //old // $amount = $invoices->sum('balance'); + /*filter only payable invoices*/ $invoices = $invoices->filter(function ($invoice) { return $invoice->isPayable(); }); + + /*return early if no invoices*/ if ($invoices->count() == 0) { return redirect() ->route('client.invoices.index') ->with(['warning' => 'No payable invoices selected.']); } + /*iterate through invoices and add gateway fees*/ foreach(request()->invoices as $payable_invoice) { - $invoice = Invoice::find($this->decodePrimaryKey($payable_invoice['invoice_id'])); + $invoice = $invoices->first(function ($inv) use($payable_invoice) { + return $payable_invoice['invoice_id'] == $inv->hashed_id; + }); - $invoice->service()->addGatewayFee($payable_invoice['amount']); + if($invoice) + $invoice->service()->addGatewayFee($payable_invoice['amount']); } - - $invoices->map(function ($invoice) { + /*Format invoices*/ + $invoices->fresh()->map(function ($invoice) { $invoice->balance = Number::formatMoney($invoice->balance, $invoice->client); $invoice->due_date = $this->formatDate($invoice->due_date, $invoice->client->date_format()); From 37c5a7adb5566a7336f2ec80c09a53dcbb5d483f Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 26 Aug 2020 08:10:49 +1000 Subject: [PATCH 03/20] Test for gateway fees appending to invoice --- .../ClientPortal/PaymentController.php | 4 +- app/Models/CompanyGateway.php | 26 +++++--- app/Services/Invoice/AddGatewayFee.php | 60 ++++++++++++++++++- app/Services/Invoice/InvoiceService.php | 2 +- tests/Feature/CompanyGatewayTest.php | 39 ++++++++++++ 5 files changed, 118 insertions(+), 13 deletions(-) diff --git a/app/Http/Controllers/ClientPortal/PaymentController.php b/app/Http/Controllers/ClientPortal/PaymentController.php index 1b1f4ac70f39..482700f1bdcd 100644 --- a/app/Http/Controllers/ClientPortal/PaymentController.php +++ b/app/Http/Controllers/ClientPortal/PaymentController.php @@ -117,10 +117,10 @@ class PaymentController extends Controller }); if($invoice) - $invoice->service()->addGatewayFee($payable_invoice['amount']); + $invoice->service()->addGatewayFee($payable_invoice['amount'])->save(); } - /*Format invoices*/ + /*Format invoices we need to use fresh() here to bring in the gateway fees*/ $invoices->fresh()->map(function ($invoice) { $invoice->balance = Number::formatMoney($invoice->balance, $invoice->client); $invoice->due_date = $this->formatDate($invoice->due_date, $invoice->client->date_format()); diff --git a/app/Models/CompanyGateway.php b/app/Models/CompanyGateway.php index e5b0cfc10be4..586187ad8df6 100644 --- a/app/Models/CompanyGateway.php +++ b/app/Models/CompanyGateway.php @@ -211,6 +211,20 @@ class CompanyGateway extends BaseModel 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 * @@ -238,15 +252,11 @@ class CompanyGateway extends BaseModel public function calcGatewayFee($amount) { - if (is_null($this->fees_and_limits)) { + + $fees_and_limits = $this->getFeesAndLimits(); + + if(!$fees_and_limits) 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; diff --git a/app/Services/Invoice/AddGatewayFee.php b/app/Services/Invoice/AddGatewayFee.php index f6e8891d9b38..8a231513ce66 100644 --- a/app/Services/Invoice/AddGatewayFee.php +++ b/app/Services/Invoice/AddGatewayFee.php @@ -45,6 +45,8 @@ class AddGatewayFee extends AbstractService { $gateway_fee = $this->company_gateway->calcGatewayFee($this->amount); + $this->cleanPendingGatewayFees(); + if($gateway_fee > 0) return $this->processGatewayFee($gateway_fee); @@ -53,15 +55,69 @@ class AddGatewayFee extends AbstractService } + 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->notes = ctrans('texts.Gateway Fee Surcharge'); + $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; + + $this->invoice = $this->invoice->calc()->getInvoice(); + + 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(); + + return $this->invoice; } } diff --git a/app/Services/Invoice/InvoiceService.php b/app/Services/Invoice/InvoiceService.php index ef36b58cb37c..f86c2736d64a 100644 --- a/app/Services/Invoice/InvoiceService.php +++ b/app/Services/Invoice/InvoiceService.php @@ -79,7 +79,7 @@ class InvoiceService public function addGatewayFee(CompanyGateway $company_gateway, float $amount) { - $this->invoice = (new AddGatewayFee($company_gateway, $this->invoice, $amoun))->run(); + $this->invoice = (new AddGatewayFee($company_gateway, $this->invoice, $amount))->run(); return $this; } diff --git a/tests/Feature/CompanyGatewayTest.php b/tests/Feature/CompanyGatewayTest.php index ebb994a88349..f874653153af 100644 --- a/tests/Feature/CompanyGatewayTest.php +++ b/tests/Feature/CompanyGatewayTest.php @@ -49,6 +49,7 @@ class CompanyGatewayTest extends TestCase $data[1]['fee_tax_rate2'] = ''; $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; @@ -107,4 +108,42 @@ class CompanyGatewayTest extends TestCase return $passes; } + + public function testFeesAreAppendedToInvoice() + { + + $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); + } } \ No newline at end of file From 79e0fa56e2ae22121828abd2522f56bde709d28d Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 26 Aug 2020 10:47:50 +1000 Subject: [PATCH 04/20] Convert currency string to float --- .../ClientPortal/InvoiceController.php | 3 +- .../ClientPortal/PaymentController.php | 79 +++++++++++-------- app/Utils/Number.php | 30 ++++++- .../authorize/credit_card_payment.blade.php | 34 ++++++-- .../ninja2020/invoices/payment.blade.php | 15 ++-- tests/Unit/NumberTest.php | 28 +++++++ 6 files changed, 136 insertions(+), 53 deletions(-) diff --git a/app/Http/Controllers/ClientPortal/InvoiceController.php b/app/Http/Controllers/ClientPortal/InvoiceController.php index 4269038b3909..91b5f3784871 100644 --- a/app/Http/Controllers/ClientPortal/InvoiceController.php +++ b/app/Http/Controllers/ClientPortal/InvoiceController.php @@ -96,7 +96,8 @@ class InvoiceController extends Controller } $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; }); diff --git a/app/Http/Controllers/ClientPortal/PaymentController.php b/app/Http/Controllers/ClientPortal/PaymentController.php index 482700f1bdcd..f76ec3ae1dc2 100644 --- a/app/Http/Controllers/ClientPortal/PaymentController.php +++ b/app/Http/Controllers/ClientPortal/PaymentController.php @@ -18,12 +18,14 @@ use App\Jobs\Invoice\InjectSignature; use App\Models\CompanyGateway; use App\Models\Invoice; use App\Models\Payment; +use App\Models\PaymentHash; use App\Utils\Number; use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesHash; use Cache; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Str; use Yajra\DataTables\Facades\DataTables; /** @@ -77,24 +79,12 @@ class PaymentController extends Controller // 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. - // [ - // 'invoices' => - // [ - // 'invoice_id' => 'xx', - // 'amount' => 'yy', - // ] - // ] - - //old - // $invoices = Invoice::whereIn('id', $this->transformKeys(request()->invoices)) - // ->where('company_id', auth('contact')->user()->company->id) - // ->get(); +// dd(request()->all()); + $gateway = CompanyGateway::find(request()->input('company_gateway_id')); /*find invoices*/ - $invoices = Invoice::whereIn('id', $this->transformKeys(array_column(request()->invoices, 'invoice_id')))->get(); - - //old - // $amount = $invoices->sum('balance'); + $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) { @@ -109,24 +99,37 @@ class PaymentController extends Controller ->with(['warning' => 'No payable invoices selected.']); } - /*iterate through invoices and add gateway fees*/ - foreach(request()->invoices as $payable_invoice) + /*iterate through invoices and add gateway fees and other payment metadata*/ + + foreach($payable_invoices as $key => $payable_invoice) { + $payable_invoice[$key]['amount'] = Number::parseFloat($payable_invoice[$key]['amount']); + $invoice = $invoices->first(function ($inv) use($payable_invoice) { - return $payable_invoice['invoice_id'] == $inv->hashed_id; + return $payable_invoice[$key]['invoice_id'] == $inv->hashed_id; }); if($invoice) - $invoice->service()->addGatewayFee($payable_invoice['amount'])->save(); - } + $invoice->service()->addGatewayFee($gateway, $payable_invoice[$key]['amount'])->save(); - /*Format invoices we need to use fresh() here to bring in the gateway fees*/ - $invoices->fresh()->map(function ($invoice) { - $invoice->balance = Number::formatMoney($invoice->balance, $invoice->client); - $invoice->due_date = $this->formatDate($invoice->due_date, $invoice->client->date_format()); - - return $invoice; - }); + /*Update the payable amount to include the fee*/ + $gateway_fee = $gateway->calcGatewayFee($payable_invoice[$key]['amount']); + + $payable_invoice[$key]['amount_with_fee'] += $gateway_fee; + $payable_invoice[$key]['fee'] = $gateway_fee; + $payable_invoice[$key]['due_date'] = $this->formatDate($invoice->due_date, $invoice->client->date_format()); + $payable_invoice[$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_invoice[$key]['additional_info'] = $additional_info; + + } if ((bool) request()->signature) { $invoices->each(function ($invoice) { @@ -135,19 +138,25 @@ class PaymentController extends Controller } $payment_methods = auth()->user()->client->getPaymentMethods($amount); - $gateway = CompanyGateway::find(request()->input('company_gateway_id')); $payment_method_id = request()->input('payment_method_id'); - // Place to calculate gateway fee. + $payment_hash = new PaymentHash; + $payment_hash->hash = Str::random(128); + $payment_hash->data = $payable_invoices; + $payment_hash->save(); + + $totals = [ + 'invoice_totals' => array_sum(array_column($payable_invoices,'amount')), + 'fee_totals' => array_sum(array_column($payable_invoices, 'fee')), + 'amount_with_fee' => array_sum(array_column($payable_invoices, 'amount_with_fee')), + ]; $data = [ - 'invoices' => $invoices, - 'amount' => $amount, - 'fee' => $gateway->calcGatewayFee($amount), - 'amount_with_fee' => $amount + $gateway->calcGatewayFee($amount), + 'payment_hash' => $payment_hash->hash, + 'total' => $totals, + 'invoices' => $payable_invoices, 'token' => auth()->user()->client->gateway_token($gateway->id, $payment_method_id), 'payment_method_id' => $payment_method_id, - 'hashed_ids' => request()->invoices, ]; return $gateway diff --git a/app/Utils/Number.php b/app/Utils/Number.php index 008a067ebb60..858af3f8ed3d 100644 --- a/app/Utils/Number.php +++ b/app/Utils/Number.php @@ -33,12 +33,12 @@ class Number /** * 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 * - * @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); @@ -49,6 +49,30 @@ class Number 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 * diff --git a/resources/views/portal/ninja2020/gateways/authorize/credit_card_payment.blade.php b/resources/views/portal/ninja2020/gateways/authorize/credit_card_payment.blade.php index 7f7c5ec2e580..b25731d22bf1 100644 --- a/resources/views/portal/ninja2020/gateways/authorize/credit_card_payment.blade.php +++ b/resources/views/portal/ninja2020/gateways/authorize/credit_card_payment.blade.php @@ -13,9 +13,7 @@ @section('body')
@csrf - @foreach($invoices as $invoice) - - @endforeach + @@ -23,7 +21,7 @@ - +
@@ -39,11 +37,23 @@ @if($tokens->count() == 0)
+
+ {{ ctrans('texts.totals') }} +
+
+ {{ App\Utils\Number::formatMoney($total['invoice_totals'], $client) }} +
+
+ {{ ctrans('texts.gateway_fees') }} +
+
+ {{ App\Utils\Number::formatMoney($total['fee_totals'], $client) }} +
{{ ctrans('texts.amount') }}
- {{ App\Utils\Number::formatMoney($amount_with_fee, $client) }} + {{ App\Utils\Number::formatMoney($total['amount_with_fee'], $client) }}
@@ -67,11 +77,23 @@
+
+ {{ ctrans('texts.totals') }} +
+
+ {{ App\Utils\Number::formatMoney($total['invoice_totals'], $client) }} +
+
+ {{ ctrans('texts.gateway_fees') }} +
+
+ {{ App\Utils\Number::formatMoney($total['fee_totals'], $client) }} +
{{ ctrans('texts.amount') }}
- {{ App\Utils\Number::formatMoney($amount_with_fee, $client) }} + {{ App\Utils\Number::formatMoney($total['amount_with_fee'], $client) }}
@foreach($tokens as $token) diff --git a/resources/views/portal/ninja2020/invoices/payment.blade.php b/resources/views/portal/ninja2020/invoices/payment.blade.php index 7bfa33a112f2..0a1838e0336c 100644 --- a/resources/views/portal/ninja2020/invoices/payment.blade.php +++ b/resources/views/portal/ninja2020/invoices/payment.blade.php @@ -10,13 +10,10 @@ @section('body')
@csrf - @foreach($invoices as $invoice) - - @endforeach -
+
@@ -63,7 +60,8 @@
- @foreach($invoices as $invoice) + @foreach($invoices as $key => $invoice) +

@@ -104,7 +102,7 @@ @elseif($invoice->public_notes) {{ $invoice->public_notes }} @else - {{ $invoice->invoice_date}} + {{ $invoice->date}} @endif

@@ -113,7 +111,8 @@ {{ ctrans('texts.amount') }}
- {{ App\Utils\Number::formatMoney($invoice->amount, $invoice->client) }} + +
@@ -123,7 +122,7 @@
- + @include('portal.ninja2020.invoices.includes.terms') @include('portal.ninja2020.invoices.includes.signature') @endsection diff --git a/tests/Unit/NumberTest.php b/tests/Unit/NumberTest.php index f46c2e7d2b8d..c2c67fd78bb9 100644 --- a/tests/Unit/NumberTest.php +++ b/tests/Unit/NumberTest.php @@ -2,6 +2,7 @@ namespace Tests\Unit; +use App\Models\Currency; use App\Utils\Number; use Tests\TestCase; @@ -31,4 +32,31 @@ class NumberTest extends TestCase $this->assertEquals(2.15, $rounded); } + + public function testParsingFloats() + { + + Currency::all()->each(function ($currency){ + + $amount = 123456789.12; + + $formatted_amount = Number::formatValue($amount, $currency); + + info($formatted_amount); + + $float_amount = Number::parseFloat($formatted_amount); + + info($float_amount); + info($currency->id); + info($currency->code); + + if($currency->precision == 0){ + $this->assertEquals(123456789, $float_amount); + } + else + $this->assertEquals($amount, $float_amount); + + }); + + } } From d57f0f923679dd63e067a96a3fe1ed4c24e51bc8 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 26 Aug 2020 10:53:11 +1000 Subject: [PATCH 05/20] Refactoring client payments --- .../Controllers/ClientPortal/PaymentController.php | 12 +++++++----- tests/Unit/NumberTest.php | 6 ------ 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/app/Http/Controllers/ClientPortal/PaymentController.php b/app/Http/Controllers/ClientPortal/PaymentController.php index f76ec3ae1dc2..036e88ad394c 100644 --- a/app/Http/Controllers/ClientPortal/PaymentController.php +++ b/app/Http/Controllers/ClientPortal/PaymentController.php @@ -103,19 +103,21 @@ class PaymentController extends Controller foreach($payable_invoices as $key => $payable_invoice) { - $payable_invoice[$key]['amount'] = Number::parseFloat($payable_invoice[$key]['amount']); + + $payable_invoice[$key]['amount'] = Number::parseFloat($payable_invoice['amount']); + $payable_invoice['amount'] = $payable_invoice[$key]['amount']; $invoice = $invoices->first(function ($inv) use($payable_invoice) { - return $payable_invoice[$key]['invoice_id'] == $inv->hashed_id; + return $payable_invoice['invoice_id'] == $inv->hashed_id; }); if($invoice) - $invoice->service()->addGatewayFee($gateway, $payable_invoice[$key]['amount'])->save(); + $invoice->service()->addGatewayFee($gateway, $payable_invoice['amount'])->save(); /*Update the payable amount to include the fee*/ - $gateway_fee = $gateway->calcGatewayFee($payable_invoice[$key]['amount']); + $gateway_fee = $gateway->calcGatewayFee($payable_invoice['amount']); - $payable_invoice[$key]['amount_with_fee'] += $gateway_fee; + $payable_invoice[$key]['amount_with_fee'] = $payable_invoice['amount'] + $gateway_fee; $payable_invoice[$key]['fee'] = $gateway_fee; $payable_invoice[$key]['due_date'] = $this->formatDate($invoice->due_date, $invoice->client->date_format()); $payable_invoice[$key]['invoice_number'] = $invoice->number; diff --git a/tests/Unit/NumberTest.php b/tests/Unit/NumberTest.php index c2c67fd78bb9..3997864da191 100644 --- a/tests/Unit/NumberTest.php +++ b/tests/Unit/NumberTest.php @@ -42,14 +42,8 @@ class NumberTest extends TestCase $formatted_amount = Number::formatValue($amount, $currency); - info($formatted_amount); - $float_amount = Number::parseFloat($formatted_amount); - info($float_amount); - info($currency->id); - info($currency->code); - if($currency->precision == 0){ $this->assertEquals(123456789, $float_amount); } From ff5d8ad8717af67d628362a6388fe90cfd9b58ad Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 26 Aug 2020 11:14:15 +1000 Subject: [PATCH 06/20] Fixes for gateway fees --- .../ClientPortal/PaymentController.php | 31 ++++++++++--------- .../authorize/credit_card_payment.blade.php | 4 +-- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/app/Http/Controllers/ClientPortal/PaymentController.php b/app/Http/Controllers/ClientPortal/PaymentController.php index 036e88ad394c..ea2ee36a4700 100644 --- a/app/Http/Controllers/ClientPortal/PaymentController.php +++ b/app/Http/Controllers/ClientPortal/PaymentController.php @@ -104,23 +104,23 @@ class PaymentController extends Controller foreach($payable_invoices as $key => $payable_invoice) { - $payable_invoice[$key]['amount'] = Number::parseFloat($payable_invoice['amount']); - $payable_invoice['amount'] = $payable_invoice[$key]['amount']; + $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; }); - if($invoice) - $invoice->service()->addGatewayFee($gateway, $payable_invoice['amount'])->save(); + // if($invoice) + // $invoice->service()->addGatewayFee($gateway, $payable_invoice['amount'])->save(); /*Update the payable amount to include the fee*/ - $gateway_fee = $gateway->calcGatewayFee($payable_invoice['amount']); + // $gateway_fee = $gateway->calcGatewayFee($payable_invoice['amount']); - $payable_invoice[$key]['amount_with_fee'] = $payable_invoice['amount'] + $gateway_fee; - $payable_invoice[$key]['fee'] = $gateway_fee; - $payable_invoice[$key]['due_date'] = $this->formatDate($invoice->due_date, $invoice->client->date_format()); - $payable_invoice[$key]['invoice_number'] = $invoice->number; + // $payable_invoices[$key]['amount_with_fee'] = $payable_invoice['amount'] + $gateway_fee; + // $payable_invoices[$key]['fee'] = $gateway_fee; + $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; @@ -129,7 +129,7 @@ class PaymentController extends Controller else $additional_info = $invoice->date; - $payable_invoice[$key]['additional_info'] = $additional_info; + $payable_invoices[$key]['additional_info'] = $additional_info; } @@ -139,7 +139,7 @@ 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'))); $payment_method_id = request()->input('payment_method_id'); $payment_hash = new PaymentHash; @@ -147,10 +147,13 @@ class PaymentController extends Controller $payment_hash->data = $payable_invoices; $payment_hash->save(); + $invoice_totals = array_sum(array_column($payable_invoices,'amount')); + $fee_totals = $gateway->calcGatewayFee($invoice_totals); + $totals = [ - 'invoice_totals' => array_sum(array_column($payable_invoices,'amount')), - 'fee_totals' => array_sum(array_column($payable_invoices, 'fee')), - 'amount_with_fee' => array_sum(array_column($payable_invoices, 'amount_with_fee')), + 'invoice_totals' => $invoice_totals, + 'fee_totals' => $fee_totals, + 'amount_with_fee' => $invoice_totals + $fee_totals, ]; $data = [ diff --git a/resources/views/portal/ninja2020/gateways/authorize/credit_card_payment.blade.php b/resources/views/portal/ninja2020/gateways/authorize/credit_card_payment.blade.php index b25731d22bf1..f7573fd09d9d 100644 --- a/resources/views/portal/ninja2020/gateways/authorize/credit_card_payment.blade.php +++ b/resources/views/portal/ninja2020/gateways/authorize/credit_card_payment.blade.php @@ -38,7 +38,7 @@
- {{ ctrans('texts.totals') }} + {{ ctrans('texts.subtotal') }}
{{ App\Utils\Number::formatMoney($total['invoice_totals'], $client) }} @@ -50,7 +50,7 @@ {{ App\Utils\Number::formatMoney($total['fee_totals'], $client) }}
- {{ ctrans('texts.amount') }} + {{ ctrans('texts.total') }}
{{ App\Utils\Number::formatMoney($total['amount_with_fee'], $client) }} From 1a7d0d3cbd648aae11b088fc665350a5fb48852e Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 27 Aug 2020 22:12:39 +1000 Subject: [PATCH 07/20] Working on gateway fees across multiple invoices --- .../ClientPortal/PaymentController.php | 25 ++++++++---- .../Payments/PaymentResponseRequest.php | 39 +++++++++++++++++++ app/Jobs/Invoice/EmailInvoice.php | 1 + app/Models/Company.php | 2 +- app/Models/CompanyGateway.php | 28 +++++++++++++ app/Models/PaymentHash.php | 1 - ...40557_add_is_public_to_documents_table.php | 1 + resources/views/index/index.blade.php | 5 +++ .../authorize/credit_card_payment.blade.php | 4 +- 9 files changed, 94 insertions(+), 12 deletions(-) create mode 100644 app/Http/Requests/ClientPortal/Payments/PaymentResponseRequest.php diff --git a/app/Http/Controllers/ClientPortal/PaymentController.php b/app/Http/Controllers/ClientPortal/PaymentController.php index ea2ee36a4700..9ff34d666c78 100644 --- a/app/Http/Controllers/ClientPortal/PaymentController.php +++ b/app/Http/Controllers/ClientPortal/PaymentController.php @@ -14,6 +14,7 @@ namespace App\Http\Controllers\ClientPortal; use App\Filters\PaymentFilters; use App\Http\Controllers\Controller; +use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest; use App\Jobs\Invoice\InjectSignature; use App\Models\CompanyGateway; use App\Models\Invoice; @@ -142,17 +143,18 @@ class PaymentController extends Controller $payment_methods = auth()->user()->client->getPaymentMethods(array_sum(array_column($payable_invoices, 'amount_with_fee'))); $payment_method_id = request()->input('payment_method_id'); - $payment_hash = new PaymentHash; - $payment_hash->hash = Str::random(128); - $payment_hash->data = $payable_invoices; - $payment_hash->save(); - $invoice_totals = array_sum(array_column($payable_invoices,'amount')); $fee_totals = $gateway->calcGatewayFee($invoice_totals); + $payment_hash = new PaymentHash; + $payment_hash->hash = Str::random(128); + $payment_hash->data = $payable_invoices; + $payment_hash->fees = $fee_totals; + $payment_hash->save(); + $totals = [ 'invoice_totals' => $invoice_totals, - 'fee_totals' => $fee_totals, + 'fee_total' => $fee_totals, 'amount_with_fee' => $invoice_totals + $fee_totals, ]; @@ -170,10 +172,17 @@ class PaymentController extends Controller ->processPaymentView($data); } - public function response(Request $request) + public function response(PaymentResponseRequest $request) { - $gateway = CompanyGateway::find($request->input('company_gateway_id')); + $gateway = CompanyGateway::find($request->input('company_gateway_id'))->firstOrFail(); + $payment_hash = $request->getPaymentHash(); + $payment_invoices = $payment_hash->invoices(); + $fee_total = $payment_hash->fee_total; + + $invoices = Invoice::whereIn('id', $this->transformKeys(array_column($payable_invoices, 'invoice_id')))->get(); + + $invoice_count = $invoices->count(); //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 diff --git a/app/Http/Requests/ClientPortal/Payments/PaymentResponseRequest.php b/app/Http/Requests/ClientPortal/Payments/PaymentResponseRequest.php new file mode 100644 index 000000000000..0302bf7dd1b9 --- /dev/null +++ b/app/Http/Requests/ClientPortal/Payments/PaymentResponseRequest.php @@ -0,0 +1,39 @@ + 'required', + 'payment_hash' => 'required', + ]; + } + + public function getPaymentHash() + { + $input = $this->all(); + + return PaymentHash::whereRaw("BINARY `hash`= ?", [$input['payment_hash']])->first(); + } +} diff --git a/app/Jobs/Invoice/EmailInvoice.php b/app/Jobs/Invoice/EmailInvoice.php index 4d7ee0380059..93b9b04239f7 100644 --- a/app/Jobs/Invoice/EmailInvoice.php +++ b/app/Jobs/Invoice/EmailInvoice.php @@ -91,6 +91,7 @@ class EmailInvoice extends BaseMailerJob implements ShouldQueue catch (\Swift_TransportException $e) { event(new InvoiceWasEmailedAndFailed($this->invoice_invitation->invoice, $this->company, $e->getMessage(), Ninja::eventVars())); + } if (count(Mail::failures()) > 0) { diff --git a/app/Models/Company.php b/app/Models/Company.php index 9027999579bd..edb4d55a6544 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -393,7 +393,7 @@ class Company extends BaseModel public function system_logs() { - return $this->hasMany(SystemLog::class); + return $this->hasMany(SystemLog::class)->orderBy('id', 'DESC')->take(50); } public function tokens_hashed() diff --git a/app/Models/CompanyGateway.php b/app/Models/CompanyGateway.php index 586187ad8df6..7db6f56bb6de 100644 --- a/app/Models/CompanyGateway.php +++ b/app/Models/CompanyGateway.php @@ -293,6 +293,34 @@ class CompanyGateway extends BaseModel return $fee; } + + 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) { return $this diff --git a/app/Models/PaymentHash.php b/app/Models/PaymentHash.php index d9c2c2541052..9fd79df9269b 100644 --- a/app/Models/PaymentHash.php +++ b/app/Models/PaymentHash.php @@ -20,7 +20,6 @@ class PaymentHash extends Model 'data' => 'object' ]; - public function invoices() { return $this->data->invoices; diff --git a/database/migrations/2020_08_18_140557_add_is_public_to_documents_table.php b/database/migrations/2020_08_18_140557_add_is_public_to_documents_table.php index 92c6b9627c22..1070bc305127 100644 --- a/database/migrations/2020_08_18_140557_add_is_public_to_documents_table.php +++ b/database/migrations/2020_08_18_140557_add_is_public_to_documents_table.php @@ -38,6 +38,7 @@ class AddIsPublicToDocumentsTable extends Migration Schema::create('payment_hashes', function ($table) { $table->increments('id'); $table->string('hash', 255); + $table->decimal('fee_total', 16, 4); $table->mediumText('data'); $table->timestamps(6); }); diff --git a/resources/views/index/index.blade.php b/resources/views/index/index.blade.php index b032dc2e70d6..ccfe1573d83f 100644 --- a/resources/views/index/index.blade.php +++ b/resources/views/index/index.blade.php @@ -11,6 +11,11 @@