diff --git a/app/DataMapper/CompanySettings.php b/app/DataMapper/CompanySettings.php index a2b8f884690a..276cdca50ff2 100644 --- a/app/DataMapper/CompanySettings.php +++ b/app/DataMapper/CompanySettings.php @@ -238,8 +238,13 @@ class CompanySettings extends BaseSettings public $client_portal_terms = ''; public $client_portal_privacy_policy = ''; public $client_portal_enable_uploads = false; + public $client_portal_allow_under_payment = false; + public $client_portal_allow_over_payment = false; + public static $casts = [ + 'client_portal_allow_under_payment' => 'bool', + 'client_portal_allow_over_payment' => 'bool', 'auto_bill' => 'string', 'lock_invoices' => 'string', 'client_portal_terms' => 'string', diff --git a/app/Http/Controllers/ClientPortal/InvoiceController.php b/app/Http/Controllers/ClientPortal/InvoiceController.php index 28ae5392b5a9..cc70d46692b1 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; }); @@ -113,6 +114,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..03d59ebe1c22 100644 --- a/app/Http/Controllers/ClientPortal/PaymentController.php +++ b/app/Http/Controllers/ClientPortal/PaymentController.php @@ -14,16 +14,19 @@ 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; 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; /** @@ -72,28 +75,56 @@ 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. + // 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) { return $invoice->isPayable(); }); + + /*return early if no invoices*/ if ($invoices->count() == 0) { return redirect() ->route('client.invoices.index') ->with(['warning' => 'No payable invoices selected.']); } - $invoices->map(function ($invoice) { - $invoice->balance = Number::formatMoney($invoice->balance, $invoice->client); - $invoice->due_date = $this->formatDate($invoice->due_date, $invoice->client->date_format()); + /*iterate through invoices and add gateway fees and other payment metadata*/ + + 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) { $invoices->each(function ($invoice) { @@ -101,20 +132,46 @@ class PaymentController extends Controller }); } - $payment_methods = auth()->user()->client->getPaymentMethods($amount); - $gateway = CompanyGateway::find(request()->input('company_gateway_id')); + $payment_methods = auth()->user()->client->getPaymentMethods(array_sum(array_column($payable_invoices, 'amount_with_fee'))); $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 = [ - '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, + 'amount_with_fee' => $invoice_totals + $fee_totals, ]; return $gateway @@ -123,10 +180,26 @@ class PaymentController extends Controller ->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 ->driver(auth()->user()->client) ->setPaymentMethod($request->input('payment_method_id')) diff --git a/app/Http/Controllers/MigrationController.php b/app/Http/Controllers/MigrationController.php index 4b4c755cb3df..cf931a19026b 100644 --- a/app/Http/Controllers/MigrationController.php +++ b/app/Http/Controllers/MigrationController.php @@ -31,7 +31,6 @@ class MigrationController extends BaseController parent::__construct(); } - /** * * Purge Company 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/Jobs/Util/Import.php b/app/Jobs/Util/Import.php index f353f22276f2..34ce39061bad 100644 --- a/app/Jobs/Util/Import.php +++ b/app/Jobs/Util/Import.php @@ -227,6 +227,14 @@ class Import implements ShouldQueue 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->save($data, $this->company); 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 e5b0cfc10be4..2f521501aedb 100644 --- a/app/Models/CompanyGateway.php +++ b/app/Models/CompanyGateway.php @@ -201,6 +201,23 @@ class CompanyGateway extends BaseModel 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 * Only works for STRIPE and PAYMILL @@ -211,6 +228,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 * @@ -236,17 +267,13 @@ class CompanyGateway extends BaseModel 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; - } - - $fees_and_limits = new \stdClass; - - foreach($this->fees_and_limits as $key => $value) { - $fees_and_limits = $this->fees_and_limits->{$key}; - } $fee = 0; @@ -259,30 +286,68 @@ class CompanyGateway extends BaseModel $fee += $amount * $fees_and_limits->fee_percent / 100; 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)) $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; } + /** + * 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) { return $this diff --git a/app/Models/PaymentHash.php b/app/Models/PaymentHash.php new file mode 100644 index 000000000000..2031b2f1aba7 --- /dev/null +++ b/app/Models/PaymentHash.php @@ -0,0 +1,29 @@ + 'object' + ]; + + public function invoices() + { + return $this->data; + } +} diff --git a/app/Observers/CompanyGatewayObserver.php b/app/Observers/CompanyGatewayObserver.php index cecd724fb924..77e6d4e8e8cd 100644 --- a/app/Observers/CompanyGatewayObserver.php +++ b/app/Observers/CompanyGatewayObserver.php @@ -14,6 +14,8 @@ class CompanyGatewayObserver */ public function created(CompanyGateway $company_gateway) { + + /* Set company gateway if not exists*/ if(!$company_gateway->label){ $company_gateway->label = $company_gateway->gateway->name; $company_gateway->save(); diff --git a/app/PaymentDrivers/BaseDriver.php b/app/PaymentDrivers/BaseDriver.php index b665a29f2d67..f95b97e9c449 100644 --- a/app/PaymentDrivers/BaseDriver.php +++ b/app/PaymentDrivers/BaseDriver.php @@ -14,6 +14,7 @@ namespace App\PaymentDrivers; use App\Events\Invoice\InvoiceWasPaid; use App\Factory\PaymentFactory; +use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest; use App\Models\Client; use App\Models\ClientGatewayToken; use App\Models\CompanyGateway; @@ -158,4 +159,40 @@ class BaseDriver extends AbstractPaymentDriver * @return Response The payment response */ public function tokenBilling(ClientGatewayToken $cgt, float $amount, ?Invoice $invoice = null) {} + + /** + * 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'); + } + + }); + + } } diff --git a/app/PaymentDrivers/BasePaymentDriver.php b/app/PaymentDrivers/BasePaymentDriver.php index 0585aaf2ee5b..81fd86fe97b6 100644 --- a/app/PaymentDrivers/BasePaymentDriver.php +++ b/app/PaymentDrivers/BasePaymentDriver.php @@ -13,6 +13,7 @@ namespace App\PaymentDrivers; use App\Factory\PaymentFactory; +use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest; use App\Models\Client; use App\Models\ClientContact; use App\Models\CompanyGateway; @@ -286,4 +287,41 @@ class BasePaymentDriver 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'); + } + + }); + + } } + diff --git a/app/PaymentDrivers/Stripe/CreditCard.php b/app/PaymentDrivers/Stripe/CreditCard.php index b0a585db6cd5..b00552e87a30 100644 --- a/app/PaymentDrivers/Stripe/CreditCard.php +++ b/app/PaymentDrivers/Stripe/CreditCard.php @@ -19,6 +19,7 @@ use App\Models\ClientGatewayToken; use App\Models\GatewayType; use App\Models\Invoice; use App\Models\Payment; +use App\Models\PaymentHash; use App\Models\PaymentType; use App\Models\SystemLog; use App\PaymentDrivers\StripePaymentDriver; @@ -92,7 +93,7 @@ class CreditCard 'amount' => $this->stripe->convertToStripeAmount($data['amount_with_fee'], $this->stripe->client->currency()->precision), 'currency' => $this->stripe->client->getCurrencyCode(), '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']) { @@ -113,6 +114,8 @@ class CreditCard { $server_response = json_decode($request->input('gateway_response')); + $payment_hash = PaymentHash::whereRaw("BINARY `hash`= ?", [$request->input('payment_hash')])->firstOrFail(); + $state = [ 'payment_method' => $server_response->payment_method, 'payment_status' => $server_response->status, @@ -120,9 +123,10 @@ class CreditCard 'gateway_type_id' => $request->payment_method_id, 'hashed_ids' => $request->hashed_ids, 'server_response' => $server_response, + 'payment_hash' => $payment_hash, ]; - $invoices = Invoice::whereIn('id', $this->stripe->transformKeys($state['hashed_ids'])) + $invoices = Invoice::whereIn('id', $this->stripe->transformKeys(array_column($payment_hash->invoices(), 'invoice_id'))) ->whereClientId($this->stripe->client->id) ->get(); @@ -138,6 +142,10 @@ class CreditCard $state['customer'] = $state['payment_intent']->customer; if ($state['payment_status'] == 'succeeded') { + + /* Add gateway fees if needed! */ + $this->stripe->confirmGatewayFee($request); + return $this->processSuccessfulPayment($state); } @@ -183,7 +191,7 @@ class CreditCard $this->stripe->attachInvoices($payment, $state['hashed_ids']); - $payment->service()->updateInvoicePayment(); + $payment->service()->updateInvoicePayment($state['payment_hash']); event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars())); diff --git a/app/Services/Invoice/AddGatewayFee.php b/app/Services/Invoice/AddGatewayFee.php new file mode 100644 index 000000000000..1566d6c47951 --- /dev/null +++ b/app/Services/Invoice/AddGatewayFee.php @@ -0,0 +1,132 @@ +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; + } +} diff --git a/app/Services/Invoice/AutoBillInvoice.php b/app/Services/Invoice/AutoBillInvoice.php index d76c5033dcdd..64ab746d9533 100644 --- a/app/Services/Invoice/AutoBillInvoice.php +++ b/app/Services/Invoice/AutoBillInvoice.php @@ -62,17 +62,16 @@ class AutoBillInvoice extends AbstractService $amount = $this->invoice->balance + $fee; } - /* Make sure we remove any stale fees*/ - $this->purgeStaleGatewayFees(); - - if($fee > 0) - $this->addFeeToInvoice($fee); - $payment = $gateway_token->gateway->driver($this->client)->tokenBilling($gateway_token, $amount, $this->invoice); if($payment){ - - $this->invoice = $this->invoice->service()->toggleFeesPaid()->save(); + + if($this->invoice->partial > 0) + $amount = $this->invoice->partial; + else + $amount = $this->invoice->balance; + + $this->invoice = $this->invoice->service()->addGatewayFee($gateway_token->gateway, $amount)->save(); } else @@ -144,34 +143,34 @@ class AutoBillInvoice extends AbstractService * * @return $this */ - private function purgeStaleGatewayFees() - { - $starting_amount = $this->invoice->amount; + // private function purgeStaleGatewayFees() + // { + // $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) - $new_items[] = $item; + // if($item->type_id != 3) + // $new_items[] = $item; - } + // } - $this->invoice->line_items = $new_items; - $this->invoice->save(); + // $this->invoice->line_items = $new_items; + // $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){ - $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(); - } + // 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->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 diff --git a/app/Services/Invoice/InvoiceService.php b/app/Services/Invoice/InvoiceService.php index 714b2727ea05..a3d729a51b18 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,11 +77,20 @@ class InvoiceService 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 + * * @param float $balance_adjustment The amount to adjust the invoice by * a negative amount will REDUCE the invoice balance, a positive amount will INCREASE * the invoice balance + * * @return InvoiceService Parent class object */ public function updateBalance($balance_adjustment) diff --git a/app/Services/Invoice/TriggeredActions.php b/app/Services/Invoice/TriggeredActions.php index e5b1bfd204f1..786c79c52342 100644 --- a/app/Services/Invoice/TriggeredActions.php +++ b/app/Services/Invoice/TriggeredActions.php @@ -56,6 +56,10 @@ class TriggeredActions extends AbstractService $this->sendEmail(); } + if($this->request->has('mark_sent') && $this->request->input('mark_sent') == 'true'){ + $this->invoice = $this->invoice->service()->markSent()->save(); + } + return $this->invoice; } diff --git a/app/Services/Invoice/UpdateBalance.php b/app/Services/Invoice/UpdateBalance.php index 34d06809f04e..6decec7a7f7e 100644 --- a/app/Services/Invoice/UpdateBalance.php +++ b/app/Services/Invoice/UpdateBalance.php @@ -37,8 +37,6 @@ class UpdateBalance extends AbstractService if ($this->invoice->balance == 0) { $this->invoice->status_id = Invoice::STATUS_PAID; - // $this->save(); - // event(new InvoiceWasPaid($this, $this->company)); } return $this->invoice; diff --git a/app/Services/Payment/PaymentService.php b/app/Services/Payment/PaymentService.php index 47f2a25fd1d7..3ac7de0e4e37 100644 --- a/app/Services/Payment/PaymentService.php +++ b/app/Services/Payment/PaymentService.php @@ -84,9 +84,9 @@ class PaymentService 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() diff --git a/app/Services/Payment/UpdateInvoicePayment.php b/app/Services/Payment/UpdateInvoicePayment.php index 8257ab3187e9..914a277ea259 100644 --- a/app/Services/Payment/UpdateInvoicePayment.php +++ b/app/Services/Payment/UpdateInvoicePayment.php @@ -17,120 +17,89 @@ use App\Jobs\Payment\EmailPayment; use App\Jobs\Util\SystemLogger; use App\Models\Invoice; use App\Models\SystemLog; +use App\Utils\Traits\MakesHash; class UpdateInvoicePayment { + use MakesHash; + /** * @deprecated This is bad logic, assumes too much. */ public $payment; - public function __construct($payment) + public $payment_hash; + + public function __construct($payment, $payment_hash) { $this->payment = $payment; + $this->payment_hash = $payment_hash; } public function run() { - $invoices = $this->payment->invoices()->get(); + // $invoices = $this->payment->invoices()->get(); + // $invoices_total = $invoices->sum('balance'); - $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*/ - 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(); + collect($paid_invoices)->each(function ($paid_invoice) use($invoices) { - $invoice->service() - ->clearPartial() - ->updateBalance($invoice->balance*-1) - ->save(); + $invoice = $invoices->first(function ($inv) use($paid_invoice) { + return $paid_invoice->invoice_id == $inv->hashed_id; }); - } - /*Combination of partials and full invoices are being paid*/ - else { - $total = 0; - /* Calculate the grand total of the invoices*/ - foreach ($invoices as $invoice) { - if ($invoice->hasPartial()) { - $total += $invoice->partial; - } else { - $total += $invoice->balance; - } - } + if($invoice->id == $this->payment_hash->fee_invoice_id) + $paid_amount = $paid_invoice->amount + $this->payment_hash->fee_total; + else + $paid_amount = $paid_invoice->amount; - /*Test if there is a batch of partial invoices that have been paid */ - if ($this->payment->amount == $total) { - $invoices->each(function ($invoice) { - if ($invoice->hasPartial()) { - $this->payment - ->ledger() - ->updatePaymentBalance($invoice->partial*-1); + $this->payment + ->ledger() + ->updatePaymentBalance($paid_amount*-1); - $this->payment->client->service() - ->updateBalance($invoice->partial*-1) - ->updatePaidToDate($invoice->partial) - ->save(); + $this->payment + ->client + ->service() + ->updateBalance($paid_amount*-1) + ->updatePaidToDate($paid_amount) + ->save(); - $invoice->pivot->amount = $invoice->partial; - $invoice->pivot->save(); + /*i think to interact with this correct - we need to do this form $payment->invoice()->pivot*/ + // $invoice->pivot->amount = $paid_amount; + // $invoice->pivot->save(); - $invoice->service()->updateBalance($invoice->partial*-1) - ->clearPartial() - ->setDueDate() - ->setStatus(Invoice::STATUS_PARTIAL) - ->save(); - } else { - $this->payment - ->ledger() - ->updatePaymentBalance($invoice->balance*-1); + $invoice->service() //caution what if we amount paid was less than partial - we wipe it! + ->clearPartial() + ->updateBalance($paid_amount*-1) + ->save(); - $this->payment->client->service() - ->updateBalance($invoice->balance*-1) - ->updatePaidToDate($invoice->balance) - ->save(); + }); - $invoice->pivot->amount = $invoice->balance; - $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 - ); + // } 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"); + // 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(); - } - } + // $this->payment->invoice()->delete(); + // $this->payment->is_deleted=true; + // $this->payment->save(); + // $this->payment->delete(); + // } + return $this->payment; } diff --git a/app/Transformers/CompanyGatewayTransformer.php b/app/Transformers/CompanyGatewayTransformer.php index e14c9ffa0d81..234190f74966 100644 --- a/app/Transformers/CompanyGatewayTransformer.php +++ b/app/Transformers/CompanyGatewayTransformer.php @@ -64,6 +64,7 @@ class CompanyGatewayTransformer extends EntityTransformer 'custom_value4' => $company_gateway->custom_value4 ?: '', 'label' => (string)$company_gateway->label ?: '', 'token_billing' => (string)$company_gateway->token_billing, + 'test_mode' => (bool)$company_gateway->isTestMode(), ]; } 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/app/Utils/SystemHealth.php b/app/Utils/SystemHealth.php index 07aa4d698f03..d5b560f6cf8b 100644 --- a/app/Utils/SystemHealth.php +++ b/app/Utils/SystemHealth.php @@ -71,8 +71,8 @@ class SystemHealth 'env_writable' => self::checkEnvWritable(), //'mail' => self::testMailServer(), 'simple_db_check' => (bool) self::simpleDbCheck(), - 'npm_status' => self::checkNpm(), - 'node_status' => self::checkNode(), + //'npm_status' => self::checkNpm(), + //'node_status' => self::checkNode(), ]; } 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..9eda7ef60bdc 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,26 @@ class AddIsPublicToDocumentsTable extends Migration $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'); + }); } 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 @@