1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-10 21:22:58 +01:00

Merge pull request #6771 from LarsK1/v5-develop

Stripe: SEPA-payment
This commit is contained in:
David Bomba 2021-10-09 08:50:03 +11:00 committed by GitHub
commit 61adddc51c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 356 additions and 75 deletions

View File

@ -103,7 +103,8 @@ class Gateway extends StaticModel
GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable','charge.succeeded']],
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']]]; //Stripe
GatewayType::SOFORT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']], //Stripe
GatewayType::SEPA => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']]];
case 39:
return [GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true]]; //Checkout
break;

View File

@ -12,98 +12,129 @@
namespace App\PaymentDrivers\Stripe;
use App\Exceptions\PaymentFailed;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\PaymentDrivers\StripePaymentDriver;
use App\Jobs\Mail\PaymentFailureMailer;
use App\Jobs\Util\SystemLogger;
use App\Models\GatewayType;
use App\Models\Payment;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\StripePaymentDriver;
use App\PaymentDrivers\Stripe\CreditCard;
use App\Utils\Ninja;
use App\Exceptions\PaymentFailed;
class SEPA
{
/** @var StripePaymentDriver */
public $stripe_driver;
public StripePaymentDriver $stripe;
public function __construct(StripePaymentDriver $stripe_driver)
public function __construct(StripePaymentDriver $stripe)
{
$this->stripe_driver = $stripe_driver;
$this->stripe = $stripe;
}
public function authorizeView(array $data)
public function authorizeView($data)
{
$customer = $this->stripe_driver->findOrCreateCustomer();
$setup_intent = \Stripe\SetupIntent::create([
'payment_method_types' => ['sepa_debit'],
'customer' => $customer->id,
], $this->stripe_driver->stripe_connect_auth);
$client_secret = $setup_intent->client_secret;
// Pass the client secret to the client
return render('gateways.stripe.sepa.authorize', $data);
}
public function paymentView(array $data) {
$data['gateway'] = $this->stripe;
$data['return_url'] = $this->buildReturnUrl();
$data['stripe_amount'] = $this->stripe->convertToStripeAmount($data['total']['amount_with_fee'], $this->stripe->client->currency()->precision, $this->stripe->client->currency());
$data['client'] = $this->stripe->client;
$data['customer'] = $this->stripe->findOrCreateCustomer()->id;
$data['country'] = $this->stripe->client->country->iso_3166_2;
return render('gateways.stripe.sepa.authorize', array_merge($data));
$intent = \Stripe\PaymentIntent::create([
'amount' => $data['stripe_amount'],
'currency' => 'eur',
'payment_method_types' => ['sepa_debit'],
'setup_future_usage' => 'off_session',
'customer' => $this->stripe->findOrCreateCustomer(),
'description' => $this->stripe->decodeUnicodeString(ctrans('texts.invoices') . ': ' . collect($data['invoices'])->pluck('invoice_number')),
]);
$data['pi_client_secret'] = $intent->client_secret;
$this->stripe->payment_hash->data = array_merge((array) $this->stripe->payment_hash->data, ['stripe_amount' => $data['stripe_amount']]);
$this->stripe->payment_hash->save();
return render('gateways.stripe.sepa.pay', $data);
}
private function buildReturnUrl(): string
{
return route('client.payments.response', [
'company_gateway_id' => $this->stripe->company_gateway->id,
'payment_hash' => $this->stripe->payment_hash->hash,
'payment_method_id' => GatewayType::SEPA,
]);
}
public function paymentResponse(PaymentResponseRequest $request)
{
$this->stripe->payment_hash->data = array_merge((array) $this->stripe->payment_hash->data, $request->all());
$this->stripe->payment_hash->save();
// $this->stripe_driver->init();
// $state = [
// 'server_response' => json_decode($request->gateway_response),
// 'payment_hash' => $request->payment_hash,
// ];
// $state['payment_intent'] = \Stripe\PaymentIntent::retrieve($state['server_response']->id, $this->stripe_driver->stripe_connect_auth);
// $state['customer'] = $state['payment_intent']->customer;
// $this->stripe_driver->payment_hash->data = array_merge((array) $this->stripe_driver->payment_hash->data, $state);
// $this->stripe_driver->payment_hash->save();
// $server_response = $this->stripe_driver->payment_hash->data->server_response;
// $response_handler = new CreditCard($this->stripe_driver);
// if ($server_response->status == 'succeeded') {
// $this->stripe_driver->logSuccessfulGatewayResponse(['response' => json_decode($request->gateway_response), 'data' => $this->stripe_driver->payment_hash], SystemLog::TYPE_STRIPE);
// return $response_handler->processSuccessfulPayment();
// }
// return $response_handler->processUnsuccessfulPayment($server_response);
if ($request->redirect_status == 'succeeded') {
return $this->processSuccessfulPayment($request->payment_intent);
}
return $this->processUnsuccessfulPayment();
}
/* Searches for a stripe customer by email
otherwise searches by gateway tokens in StripePaymentdriver
finally creates a new customer if none found
*/
private function getCustomer()
public function processSuccessfulPayment(string $payment_intent)
{
$searchResults = \Stripe\Customer::all([
"email" => $this->stripe_driver->client->present()->email(),
"limit" => 1,
"starting_after" => null
], $this->stripe_driver->stripe_connect_auth);
$this->stripe->init();
if(count($searchResults) >= 1)
return $searchResults[0];
$data = [
'payment_method' => $payment_intent,
'payment_type' => PaymentType::SEPA,
'amount' => $this->stripe->convertFromStripeAmount($this->stripe->payment_hash->data->stripe_amount, $this->stripe->client->currency()->precision, $this->stripe->client->currency()),
'transaction_reference' => $payment_intent,
'gateway_type_id' => GatewayType::SEPA,
];
return $this->stripe_driver->findOrCreateCustomer();
$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()->route('client.payments.index');
}
public function processUnsuccessfulPayment()
{
$server_response = $this->stripe->payment_hash->data;
PaymentFailureMailer::dispatch(
$this->stripe->client,
$server_response,
$this->stripe->client->company,
$this->stripe->convertFromStripeAmount($this->stripe->payment_hash->data->stripe_amount, $this->stripe->client->currency()->precision, $this->stripe->client->currency())
);
$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);
}
}

View File

@ -28,6 +28,7 @@ use App\PaymentDrivers\Stripe\Alipay;
use App\PaymentDrivers\Stripe\Charge;
use App\PaymentDrivers\Stripe\CreditCard;
use App\PaymentDrivers\Stripe\SOFORT;
use App\PaymentDrivers\Stripe\SEPA;
use App\PaymentDrivers\Stripe\Utilities;
use App\Utils\Traits\MakesHash;
use Exception;

View File

@ -32,6 +32,7 @@ use App\PaymentDrivers\Stripe\Connect\Verify;
use App\PaymentDrivers\Stripe\CreditCard;
use App\PaymentDrivers\Stripe\ImportCustomers;
use App\PaymentDrivers\Stripe\SOFORT;
use App\PaymentDrivers\Stripe\SEPA;
use App\PaymentDrivers\Stripe\UpdatePaymentMethods;
use App\PaymentDrivers\Stripe\Utilities;
use App\Utils\Traits\MakesHash;
@ -75,7 +76,7 @@ class StripePaymentDriver extends BaseDriver
GatewayType::ALIPAY => Alipay::class,
GatewayType::SOFORT => SOFORT::class,
GatewayType::APPLE_PAY => ApplePay::class,
GatewayType::SEPA => 1, // TODO
GatewayType::SEPA => SEPA::class,
];
const SYSTEM_LOG_TYPE = SystemLog::TYPE_STRIPE;
@ -125,7 +126,7 @@ class StripePaymentDriver extends BaseDriver
$types = [
// GatewayType::CRYPTO,
GatewayType::CREDIT_CARD
];
];
if ($this->client
&& isset($this->client->country)
@ -146,6 +147,12 @@ class StripePaymentDriver extends BaseDriver
$types[] = GatewayType::ALIPAY;
}
if ($this->client
&& isset($this->client->country)
&& in_array($this->client->country->iso_3166_3, ['AUS', 'DNK', 'DEU', 'ITA', 'LUX', 'NOR', 'SVN', 'GBR', 'EST', 'GRC', 'JPN', 'PRT', 'ESP', 'USA', 'BEL', 'FIN'])) { // TODO: More has to be added https://stripe.com/docs/payments/sepa-debit
$types[] = GatewayType::SEPA;
}
return $types;
}
@ -326,7 +333,7 @@ class StripePaymentDriver extends BaseDriver
if($customer)
return $customer;
}
}
//Search by email
$searchResults = \Stripe\Customer::all([
@ -337,11 +344,11 @@ class StripePaymentDriver extends BaseDriver
if(count($searchResults) == 1)
return $searchResults->data[0];
//Else create a new record
$data['name'] = $this->client->present()->name();
$data['phone'] = $this->client->present()->phone();
if (filter_var($this->client->present()->email(), FILTER_VALIDATE_EMAIL)) {
$data['email'] = $this->client->present()->email();
}
@ -370,7 +377,7 @@ class StripePaymentDriver extends BaseDriver
// ->create(['charge' => $payment->transaction_reference, 'amount' => $this->convertToStripeAmount($amount, $this->client->currency()->precision, $this->client->currency())], $meta);
$response = \Stripe\Refund::create([
'charge' => $payment->transaction_reference,
'charge' => $payment->transaction_reference,
'amount' => $this->convertToStripeAmount($amount, $this->client->currency()->precision, $this->client->currency())
], $meta);

View File

@ -0,0 +1,91 @@
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
class ProcessSEPA {
constructor(key, stripeConnect) {
this.key = key;
this.errors = document.getElementById('errors');
this.stripeConnect = stripeConnect;
}
setupStripe = () => {
this.stripe = Stripe(this.key);
if(this.stripeConnect)
this.stripe.stripeAccount = stripeConnect;
const elements = this.stripe.elements();
var style = {
base: {
color: "#32325d",
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
fontSmoothing: "antialiased",
fontSize: "16px",
"::placeholder": {
color: "#aab7c4"
},
":-webkit-autofill": {
color: "#32325d"
}
},
invalid: {
color: "#fa755a",
iconColor: "#fa755a",
":-webkit-autofill": {
color: "#fa755a"
}
}
};
var options = {
style: style,
supportedCountries: ["SEPA"],
// If you know the country of the customer, you can optionally pass it to
// the Element as placeholderCountry. The example IBAN that is being used
// as placeholder reflects the IBAN format of that country.
placeholderCountry: "DE"
};
this.iban = elements.create("iban", options);
this.iban.mount("#sepa-iban");
return this;
};
handle = () => {
document.getElementById('pay-now').addEventListener('click', (e) => {
document.getElementById('pay-now').disabled = true;
document.querySelector('#pay-now > svg').classList.remove('hidden');
document.querySelector('#pay-now > span').classList.add('hidden');
this.stripe.confirmSepaDebitPayment(
document.querySelector('meta[name=pi-client-secret').content,
{
payment_method: {
sepa_debit: this.iban,
billing_details: {
name: document.getElementById("sepa-name").value,
email: document.getElementById("sepa-email-address").value,
},
},
return_url: document.querySelector(
'meta[name="return-url"]'
).content,
}
);
});
};
}
const publishableKey = document.querySelector(
'meta[name="stripe-publishable-key"]'
)?.content ?? '';
const stripeConnect =
document.querySelector('meta[name="stripe-account-id"]')?.content ?? '';
new ProcessSEPA(publishableKey, stripeConnect).setupStripe().handle();

View File

@ -0,0 +1,9 @@
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/

View File

@ -0,0 +1,91 @@
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
class ProcessSEPA {
constructor(key, stripeConnect) {
this.key = key;
this.errors = document.getElementById('errors');
this.stripeConnect = stripeConnect;
}
setupStripe = () => {
this.stripe = Stripe(this.key);
if(this.stripeConnect)
this.stripe.stripeAccount = stripeConnect;
const elements = this.stripe.elements();
var style = {
base: {
color: "#32325d",
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
fontSmoothing: "antialiased",
fontSize: "16px",
"::placeholder": {
color: "#aab7c4"
},
":-webkit-autofill": {
color: "#32325d"
}
},
invalid: {
color: "#fa755a",
iconColor: "#fa755a",
":-webkit-autofill": {
color: "#fa755a"
}
}
};
var options = {
style: style,
supportedCountries: ["SEPA"],
// If you know the country of the customer, you can optionally pass it to
// the Element as placeholderCountry. The example IBAN that is being used
// as placeholder reflects the IBAN format of that country.
placeholderCountry: "DE"
};
this.iban = elements.create("iban", options);
this.iban.mount("#sepa-iban");
return this;
};
handle = () => {
document.getElementById('pay-now').addEventListener('click', (e) => {
document.getElementById('pay-now').disabled = true;
document.querySelector('#pay-now > svg').classList.remove('hidden');
document.querySelector('#pay-now > span').classList.add('hidden');
this.stripe.confirmSepaDebitPayment(
document.querySelector('meta[name=pi-client-secret').content,
{
payment_method: {
sepa_debit: this.iban,
billing_details: {
name: document.getElementById("sepa-name").value,
email: document.getElementById("sepa-email-address").value,
},
},
return_url: document.querySelector(
'meta[name="return-url"]'
).content,
}
);
});
};
}
const publishableKey = document.querySelector(
'meta[name="stripe-publishable-key"]'
)?.content ?? '';
const stripeConnect =
document.querySelector('meta[name="stripe-account-id"]')?.content ?? '';
new ProcessSEPA(publishableKey, stripeConnect).setupStripe().handle();

View File

@ -1779,7 +1779,7 @@ $LANG = array(
'lang_Bulgarian' => 'Bulgarian',
'lang_Russian (Russia)' => 'Russian (Russia)',
// Industries
'industry_Accounting & Legal' => 'Accounting & Legal',
'industry_Advertising' => 'Advertising',
@ -4316,7 +4316,9 @@ $LANG = array(
'payment_method_cannot_be_preauthorized' => 'This payment method cannot be preauthorized.',
'kbc_cbc' => 'KBC/CBC',
'bancontact' => 'Bancontact',
'sepa_mandat' => 'By providing your IBAN and confirming this payment, you are authorizing Rocketship Inc. and Stripe, our payment service provider, to send instructions to your bank to debit your account and your bank to debit your account in accordance with those instructions. You are entitled to a refund from your bank under the terms and conditions of your agreement with your bank. A refund must be claimed within 8 weeks starting from the date on which your account was debited.',
'ideal' => 'iDEAL',
'bank_account_holder' => 'Bank Account Holder',
'aio_checkout' => 'All-in-one checkout',
);

View File

@ -1,4 +1,4 @@
@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'SEPA', 'card_title' => 'SEPA'])
@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'SEPA', 'card_title' => 'SEPA-Lastschrift'])
@section('gateway_head')
@if($gateway->company_gateway->getConfigField('account_id'))
@ -10,9 +10,9 @@
@endsection
@section('gateway_content')
@if(session()->has('ach_error'))
@if(session()->has('sepa_error'))
<div class="alert alert-failure mb-4">
<p>{{ session('ach_error') }}</p>
<p>{{ session('sepa_error') }}</p>
</div>
@endif
@ -78,5 +78,5 @@
@section('gateway_footer')
<script src="https://js.stripe.com/v3/"></script>
<script src="{{ asset('js/clients/payments/stripe-ach.js') }}"></script>
<script src="{{ asset('js/clients/payments/stripe-sepa.js') }}"></script>
@endsection

View File

@ -0,0 +1,30 @@
@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'SEPA', 'card_title' => 'SEPA'])
@section('gateway_head')
<meta name="stripe-publishable-key" content="{{ $gateway->getPublishableKey() }}">
<meta name="stripe-account-id" content="{{ $gateway->company_gateway->getConfigField('account_id') }}">
<meta name="return-url" content="{{ $return_url }}">
<meta name="amount" content="{{ $stripe_amount }}">
<meta name="country" content="{{ $country }}">
<meta name="customer" content="{{ $customer }}">
<meta name="pi-client-secret" content="{{ $pi_client_secret }}">
@endsection
@section('gateway_content')
<div class="alert alert-failure mb-4" hidden id="errors"></div>
@include('portal.ninja2020.gateways.includes.payment_details')
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.payment_type')])
{{ ctrans('texts.sepa') }} ({{ ctrans('texts.bank_transfer') }})
@endcomponent
@include('portal.ninja2020.gateways.stripe.sepa.sepa_debit')
@include('portal.ninja2020.gateways.includes.pay_now')
@endsection
@push('footer')
<script src="https://js.stripe.com/v3/"></script>
<script src="{{ asset('js/clients/payments/stripe-sepa.js') }}"></script>
@endpush

View File

@ -0,0 +1,19 @@
<div id="stripe--payment-container">
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.name')])
<label for="sepa-name">
<input class="input w-full" id="sepa-name" type="text" placeholder="{{ ctrans('texts.bank_account_holder') }}">
</label>
<label for="sepa-email" >
<input class="input w-full" id="sepa-email-address" type="email" placeholder="{{ ctrans('texts.email') }}">
</label>
<label>
<div class="border p-4 rounded">
<div id="sepa-iban"></div>
</div>
</label>
<div id="mandate-acceptance">
<input type="checkbox" id="sepa-mandate-acceptance" class="input mr-4">
<label for="sepa-mandate-acceptance">{{ctrans('texts.sepa_mandat', ['company' => auth('contact')->user()->company->present()->name()])}}</label>
</div>
@endcomponent
</div>

View File

@ -18,7 +18,6 @@
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.payment_type')])
{{ ctrans('texts.sofort') }} ({{ ctrans('texts.bank_transfer') }})
@endcomponent
@include('portal.ninja2020.gateways.includes.pay_now')
@endsection