From 885ee633d78ce7b53bb0a80aff8169e4ae857b84 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 23 Feb 2023 16:52:45 +1100 Subject: [PATCH] Bank Transfers --- app/Models/Gateway.php | 2 + app/PaymentDrivers/Stripe/BankTransfer.php | 211 ++++++++++++++++++ .../PaymentIntentPartiallyFundedWebhook.php | 95 ++++++++ .../Stripe/Jobs/PaymentIntentWebhook.php | 33 --- app/PaymentDrivers/StripePaymentDriver.php | 97 ++++---- .../stripe/bank_transfer/pay.blade.php | 93 ++++++++ 6 files changed, 458 insertions(+), 73 deletions(-) create mode 100644 app/PaymentDrivers/Stripe/BankTransfer.php create mode 100644 app/PaymentDrivers/Stripe/Jobs/PaymentIntentPartiallyFundedWebhook.php create mode 100644 resources/views/portal/ninja2020/gateways/stripe/bank_transfer/pay.blade.php diff --git a/app/Models/Gateway.php b/app/Models/Gateway.php index 5244510f2d40..87b5d2bb33f8 100644 --- a/app/Models/Gateway.php +++ b/app/Models/Gateway.php @@ -104,6 +104,7 @@ class Gateway extends StaticModel return [ GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true, 'webhooks' => ['payment_intent.succeeded']], GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'customer.source.updated','payment_intent.processing']], + GatewayType::DIRECT_DEBIT => ['refund' => false, 'token_billing' => false, 'webhooks' => ['payment_intent.processing','payment_intent.succeeded','payment_intent.partially_funded']], GatewayType::ALIPAY => ['refund' => false, 'token_billing' => false], GatewayType::APPLE_PAY => ['refund' => false, 'token_billing' => false], GatewayType::SOFORT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']], @@ -141,6 +142,7 @@ class Gateway extends StaticModel return [ GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true, 'webhooks' => ['payment_intent.succeeded']], GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'customer.source.updated','payment_intent.processing']], + GatewayType::DIRECT_DEBIT => ['refund' => false, 'token_billing' => false, 'webhooks' => ['payment_intent.processing','payment_intent.succeeded','payment_intent.partially_funded']], GatewayType::ALIPAY => ['refund' => false, 'token_billing' => false], GatewayType::APPLE_PAY => ['refund' => false, 'token_billing' => false], GatewayType::SOFORT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']], diff --git a/app/PaymentDrivers/Stripe/BankTransfer.php b/app/PaymentDrivers/Stripe/BankTransfer.php new file mode 100644 index 000000000000..d04ce3dc2157 --- /dev/null +++ b/app/PaymentDrivers/Stripe/BankTransfer.php @@ -0,0 +1,211 @@ +stripe = $stripe; + } + + public function paymentView(array $data) + { + $this->stripe->init(); + + $intent = \Stripe\PaymentIntent::create([ + 'amount' => $this->stripe->convertToStripeAmount($data['total']['amount_with_fee'], $this->stripe->client->currency()->precision, $this->stripe->client->currency()), + 'currency' => $this->stripe->client->currency()->code, + 'customer' => $this->stripe->findOrCreateCustomer()->id, + 'description' => $this->stripe->getDescription(false), + 'payment_method_types' => ['customer_balance'], + 'payment_method_data' => [ + 'type' => 'customer_balance', + ], + 'payment_method_options' => [ + 'customer_balance' => [ + 'funding_type' => 'bank_transfer', + 'bank_transfer' => $this->resolveBankType() + ], + ], + 'metadata' => [ + 'payment_hash' => $this->stripe->payment_hash->hash, + 'gateway_type_id' => GatewayType::DIRECT_DEBIT, + ], + ], $this->stripe->stripe_connect_auth); + + + $this->stripe->payment_hash->data = array_merge((array) $this->stripe->payment_hash->data, ['stripe_amount' => $this->stripe->convertToStripeAmount($data['total']['amount_with_fee'], $this->stripe->client->currency()->precision, $this->stripe->client->currency())]); + $this->stripe->payment_hash->save(); + + $data = []; + $data['return_url'] = $this->buildReturnUrl(); + $data['gateway'] = $this->stripe; + $data['client_secret'] = $intent ? $intent->client_secret : false; + + return render('gateways.stripe.bank_transfer.pay', $data); + } + + /** + * Resolve the bank type based on the currency + * + * @return void + */ + private function resolveBankType() + { + + return match($this->stripe->client->currency()->code){ + 'GBP' => ['type' => 'gb_bank_transfer'], + 'EUR' => ['type' => 'eu_bank_transfer', 'eu_bank_transfer' => ['country' => $this->stripe->client->country->iso_3166_2]], + 'JPY' => ['type' => 'jp_bank_transfer'], + 'MXN' => ['type' =>'mx_bank_transfer'], + }; + + } + + /** + * Return URL + * + * @return string + */ + private function buildReturnUrl(): string + { + return route('client.payments.response.get', [ + 'company_gateway_id' => $this->stripe->company_gateway->id, + 'payment_hash' => $this->stripe->payment_hash->hash, + 'payment_method_id' => GatewayType::DIRECT_DEBIT, + ]); + } + + + public function paymentResponse(PaymentResponseRequest $request) + { + + $this->stripe->init(); + + $this->stripe->setPaymentHash($request->getPaymentHash()); + $this->stripe->client = $this->stripe->payment_hash->fee_invoice->client; + + if($request->payment_intent){ + + $pi = \Stripe\PaymentIntent::retrieve( + $request->payment_intent, + $this->stripe->stripe_connect_auth + ); + + nlog($pi); + + if (in_array($pi->status, ['succeeded', 'processing'])) { + return $this->processSuccesfulRedirect($pi); + } + + /* Create a pending payment */ + if($pi->status == 'requires_action') { + + $data = [ + 'payment_method' => $pi->payment_method, + 'payment_type' => PaymentType::DIRECT_DEBIT, + 'amount' => $this->stripe->convertFromStripeAmount($this->stripe->payment_hash->data->stripe_amount, $this->stripe->client->currency()->precision, $this->stripe->client->currency()), + 'transaction_reference' => $pi->id, + 'gateway_type_id' => GatewayType::DIRECT_DEBIT, + + ]; + + $payment = $this->stripe->createPayment($data, Payment::STATUS_PENDING); + + SystemLogger::dispatch( + ['response' => $this->stripe->payment_hash->data, 'data' => $data], + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_SUCCESS, + SystemLog::TYPE_STRIPE, + $this->stripe->client, + $this->stripe->client->company, + ); + + return redirect($pi->next_action->display_bank_transfer_instructions->hosted_instructions_url); + + } + return $this->processUnsuccesfulRedirect(); + + } + + } + + public function processSuccesfulRedirect($payment_intent) + { + $this->stripe->init(); + + $data = [ + 'payment_method' => $payment_intent->payment_method, + 'payment_type' => PaymentType::DIRECT_DEBIT, + 'amount' => $this->stripe->convertFromStripeAmount($this->stripe->payment_hash->data->stripe_amount, $this->stripe->client->currency()->precision, $this->stripe->client->currency()), + 'transaction_reference' => $payment_intent->id, + 'gateway_type_id' => GatewayType::DIRECT_DEBIT, + + ]; + + $payment = $this->stripe->createPayment($data, $payment_intent->status == 'processing' ? Payment::STATUS_PENDING : Payment::STATUS_COMPLETED); + + SystemLogger::dispatch( + ['response' => $this->stripe->payment_hash->data, 'data' => $data], + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_SUCCESS, + SystemLog::TYPE_STRIPE, + $this->stripe->client, + $this->stripe->client->company, + ); + + return redirect()->route('client.payments.show', ['payment' => $this->stripe->encodePrimaryKey($payment->id)]); + } + + public function processUnsuccesfulRedirect() + { + $server_response = $this->stripe->payment_hash->data; + + $this->stripe->sendFailureMail($server_response->redirect_status); + + $message = [ + 'server_response' => $server_response, + 'data' => $this->stripe->payment_hash->data, + ]; + + SystemLogger::dispatch( + $message, + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_FAILURE, + SystemLog::TYPE_STRIPE, + $this->stripe->client, + $this->stripe->client->company, + ); + + throw new PaymentFailed('Failed to process the payment.', 500); + } + + +} \ No newline at end of file diff --git a/app/PaymentDrivers/Stripe/Jobs/PaymentIntentPartiallyFundedWebhook.php b/app/PaymentDrivers/Stripe/Jobs/PaymentIntentPartiallyFundedWebhook.php new file mode 100644 index 000000000000..382ac5569110 --- /dev/null +++ b/app/PaymentDrivers/Stripe/Jobs/PaymentIntentPartiallyFundedWebhook.php @@ -0,0 +1,95 @@ +company_key); + + $company = Company::where('company_key', $this->company_key)->first(); + + foreach ($this->stripe_request as $transaction) { + $payment_intent = false; + + if (array_key_exists('payment_intent', $transaction)) { + $payment_intent = $transaction['payment_intent']; + } else { + $payment_intent = $transaction['id']; + } + + if(!$payment_intent){ + nlog("payment intent not found"); + nlog($transaction); + return; + } + + $payment = Payment::query() + ->where('company_id', $company->id) + ->where('transaction_reference', $payment_intent) + ->first(); + + if(!$payment){ + nlog("paymentintent found but no payment"); + } + + $company_gateway = CompanyGateway::find($this->company_gateway_id); + $stripe_driver = $company_gateway->driver()->init(); + + $hash = isset($transaction['metadata']['payment_hash']) ? $transaction['metadata']['payment_hash'] : false; + + if (!$hash) { + nlog("no hash found"); + return; + } + + $payment_hash = PaymentHash::where('hash', $hash)->first(); + + if (!$payment_hash) { + nlog("no payment hash found"); + return; + } + + $stripe_driver->client = $payment_hash->fee_invoice->client; + + $pi = \Stripe\PaymentIntent::retrieve($payment_intent, $stripe_driver->stripe_connect_auth); + + $amount = $stripe_driver->convertFromStripeAmount($pi->amount, $stripe_driver->client->currency()->precision, $stripe_driver->client->currency()->precision); + $amount_received = $stripe_driver->convertFromStripeAmount($pi->amount_received, $stripe_driver->client->currency()->precision, $stripe_driver->client->currency()->precision); + + //at this point we just send notification emails to the client and advise of over/under payments. + } + } +} diff --git a/app/PaymentDrivers/Stripe/Jobs/PaymentIntentWebhook.php b/app/PaymentDrivers/Stripe/Jobs/PaymentIntentWebhook.php index 5881d1196d82..92b7f6b81419 100644 --- a/app/PaymentDrivers/Stripe/Jobs/PaymentIntentWebhook.php +++ b/app/PaymentDrivers/Stripe/Jobs/PaymentIntentWebhook.php @@ -260,39 +260,6 @@ class PaymentIntentWebhook implements ShouldQueue } } - // private function updateSepaPayment($payment_hash, $client, $meta) - // { - - // $company_gateway = CompanyGateway::find($this->company_gateway_id); - // $payment_method_type = GatewayType::SEPA; - // $driver = $company_gateway->driver($client)->init()->setPaymentMethod($payment_method_type); - - // $payment_hash->data = array_merge((array) $payment_hash->data, $this->stripe_request); - // $payment_hash->save(); - // $driver->setPaymentHash($payment_hash); - - // $data = [ - // 'payment_method' => $payment_hash->data->object->payment_method, - // 'payment_type' => PaymentType::parseCardType(strtolower($meta['card_details'])) ?: PaymentType::CREDIT_CARD_OTHER, - // 'amount' => $payment_hash->data->amount_with_fee, - // 'transaction_reference' => $meta['transaction_reference'], - // 'gateway_type_id' => GatewayType::CREDIT_CARD, - // ]; - - // $payment = $driver->createPayment($data, Payment::STATUS_COMPLETED); - - // SystemLogger::dispatch( - // ['response' => $this->stripe_request, 'data' => $data], - // SystemLog::CATEGORY_GATEWAY_RESPONSE, - // SystemLog::EVENT_GATEWAY_SUCCESS, - // SystemLog::TYPE_STRIPE, - // $client, - // $client->company, - // ); - - - // } - private function updateCreditCardPayment($payment_hash, $client, $meta) { diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php index 2e0a352d53f4..99c8e1f3c404 100644 --- a/app/PaymentDrivers/StripePaymentDriver.php +++ b/app/PaymentDrivers/StripePaymentDriver.php @@ -12,51 +12,53 @@ namespace App\PaymentDrivers; -use App\Exceptions\PaymentFailed; -use App\Exceptions\StripeConnectFailure; -use App\Http\Requests\Payments\PaymentWebhookRequest; -use App\Http\Requests\Request; -use App\Jobs\Util\SystemLogger; -use App\Models\ClientGatewayToken; -use App\Models\GatewayType; -use App\Models\Payment; -use App\Models\PaymentHash; -use App\Models\SystemLog; -use App\PaymentDrivers\Stripe\ACH; -use App\PaymentDrivers\Stripe\ACSS; -use App\PaymentDrivers\Stripe\Alipay; -use App\PaymentDrivers\Stripe\Bancontact; -use App\PaymentDrivers\Stripe\BECS; -use App\PaymentDrivers\Stripe\BrowserPay; -use App\PaymentDrivers\Stripe\Charge; -use App\PaymentDrivers\Stripe\Connect\Verify; -use App\PaymentDrivers\Stripe\CreditCard; -use App\PaymentDrivers\Stripe\EPS; -use App\PaymentDrivers\Stripe\FPX; -use App\PaymentDrivers\Stripe\GIROPAY; -use App\PaymentDrivers\Stripe\iDeal; -use App\PaymentDrivers\Stripe\ImportCustomers; -use App\PaymentDrivers\Stripe\Jobs\PaymentIntentFailureWebhook; -use App\PaymentDrivers\Stripe\Jobs\PaymentIntentProcessingWebhook; -use App\PaymentDrivers\Stripe\Jobs\PaymentIntentWebhook; -use App\PaymentDrivers\Stripe\Klarna; -use App\PaymentDrivers\Stripe\PRZELEWY24; -use App\PaymentDrivers\Stripe\SEPA; -use App\PaymentDrivers\Stripe\SOFORT; -use App\PaymentDrivers\Stripe\UpdatePaymentMethods; -use App\PaymentDrivers\Stripe\Utilities; -use App\Utils\Traits\MakesHash; use Exception; -use Illuminate\Http\RedirectResponse; -use Laracasts\Presenter\Exceptions\PresenterException; +use Stripe\Stripe; use Stripe\Account; use Stripe\Customer; -use Stripe\Exception\ApiErrorException; +use App\Models\Payment; +use Stripe\SetupIntent; +use Stripe\StripeClient; +use App\Models\SystemLog; use Stripe\PaymentIntent; use Stripe\PaymentMethod; -use Stripe\SetupIntent; -use Stripe\Stripe; -use Stripe\StripeClient; +use App\Models\GatewayType; +use App\Models\PaymentHash; +use App\Http\Requests\Request; +use App\Jobs\Util\SystemLogger; +use App\Utils\Traits\MakesHash; +use App\Exceptions\PaymentFailed; +use App\Models\ClientGatewayToken; +use App\PaymentDrivers\Stripe\ACH; +use App\PaymentDrivers\Stripe\EPS; +use App\PaymentDrivers\Stripe\FPX; +use App\PaymentDrivers\Stripe\ACSS; +use App\PaymentDrivers\Stripe\BECS; +use App\PaymentDrivers\Stripe\SEPA; +use App\PaymentDrivers\Stripe\iDeal; +use App\PaymentDrivers\Stripe\Alipay; +use App\PaymentDrivers\Stripe\Charge; +use App\PaymentDrivers\Stripe\Klarna; +use App\PaymentDrivers\Stripe\SOFORT; +use Illuminate\Http\RedirectResponse; +use App\PaymentDrivers\Stripe\GIROPAY; +use Stripe\Exception\ApiErrorException; +use App\Exceptions\StripeConnectFailure; +use App\PaymentDrivers\Stripe\Utilities; +use App\PaymentDrivers\Stripe\Bancontact; +use App\PaymentDrivers\Stripe\BrowserPay; +use App\PaymentDrivers\Stripe\CreditCard; +use App\PaymentDrivers\Stripe\PRZELEWY24; +use App\PaymentDrivers\Stripe\BankTransfer; +use App\PaymentDrivers\Stripe\Connect\Verify; +use App\PaymentDrivers\Stripe\ImportCustomers; +use App\PaymentDrivers\Stripe\UpdatePaymentMethods; +use App\Http\Requests\Payments\PaymentWebhookRequest; +use Laracasts\Presenter\Exceptions\PresenterException; +use App\PaymentDrivers\Stripe\Jobs\PaymentIntentWebhook; +use App\PaymentDrivers\Stripe\Jobs\PaymentIntentFailureWebhook; +use App\PaymentDrivers\Stripe\Jobs\PaymentIntentProcessingWebhook; +use App\PaymentDrivers\Stripe\Jobs\PaymentIntentPartiallyFundedWebhook; class StripePaymentDriver extends BaseDriver { @@ -95,6 +97,7 @@ class StripePaymentDriver extends BaseDriver GatewayType::ACSS => ACSS::class, GatewayType::FPX => FPX::class, GatewayType::KLARNA => Klarna::class, + GatewayType::DIRECT_DEBIT => BankTransfer::class, ]; const SYSTEM_LOG_TYPE = SystemLog::TYPE_STRIPE; @@ -255,6 +258,14 @@ class StripePaymentDriver extends BaseDriver $types[] = GatewayType::APPLE_PAY; } + if ( + $this->client + && isset($this->client->country) + && in_array($this->client->country->iso_3166_2, ['FR', 'IE', 'NL', 'GB', 'DE', 'ES', 'JP', 'MX']) + ) { + $types[] = GatewayType::DIRECT_DEBIT; + } + return $types; } @@ -657,6 +668,12 @@ class StripePaymentDriver extends BaseDriver return response()->json([], 200); } + if ($request->type === 'payment_intent.partially_funded') { + PaymentIntentPartiallyFundedWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(rand(5, 10))); + + return response()->json([], 200); + } + if (in_array($request->type, ['payment_intent.payment_failed', 'charge.failed'])) { PaymentIntentFailureWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(rand(5, 10))); diff --git a/resources/views/portal/ninja2020/gateways/stripe/bank_transfer/pay.blade.php b/resources/views/portal/ninja2020/gateways/stripe/bank_transfer/pay.blade.php new file mode 100644 index 000000000000..d29504e95e3b --- /dev/null +++ b/resources/views/portal/ninja2020/gateways/stripe/bank_transfer/pay.blade.php @@ -0,0 +1,93 @@ +@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'Bank Transfer', 'card_title' => 'Bank Transfer']) + +@section('gateway_head') + @if($gateway->company_gateway->getConfigField('account_id')) + + + @else + + @endif + + +@endsection + +@section('gateway_content') + + +
+ @csrf + +
+ +
+ +
+ +
+
+ +@endsection + +@push('footer') + + + +@endpush \ No newline at end of file