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

Merge pull request #4439 from beganovich/v5-checkout-3ds

(v5) Support for Checkout.com 3D Secure 2 API
This commit is contained in:
Benjamin Beganović 2020-12-07 15:35:28 +01:00 committed by GitHub
commit 4f2bf09c0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 119 additions and 83 deletions

View File

@ -4,6 +4,7 @@ $finder = Symfony\Component\Finder\Finder::create()
->notPath('vendor')
->notPath('bootstrap')
->notPath('storage')
->notPath('node_modules')
->in(__DIR__)
->name('*.php')
->notName('*.blade.php');

View File

@ -13,7 +13,14 @@ class PaymentFailed extends Exception
public function render($request)
{
return render('gateways.unsuccessful', [
if (auth()->user()) {
return render('gateways.unsuccessful', [
'message' => $this->getMessage(),
'code' => $this->getCode(),
]);
}
return response([
'message' => $this->getMessage(),
'code' => $this->getCode(),
]);

View File

@ -13,8 +13,6 @@
namespace App\Http\Controllers;
use App\Http\Requests\Payments\PaymentWebhookRequest;
use App\Models\Payment;
use Illuminate\Support\Arr;
class PaymentWebhookController extends Controller
{
@ -23,36 +21,10 @@ class PaymentWebhookController extends Controller
$this->middleware('guest');
}
public function __invoke(PaymentWebhookRequest $request, string $company_key = null, string $gateway_key = null)
public function __invoke(PaymentWebhookRequest $request, string $gateway_key, string $company_key)
{
$transaction_reference = $this->getTransactionReference($request->all(), $request);
$payment = Payment::where('transaction_reference', $transaction_reference)->first();
if (is_null($payment)) {
return response([
'message' => 'Sorry, we couldn\'t find requested payment.',
'status_code' => 404,
], 404); /* Record event, throw an exception.. */
}
return $request
->companyGateway()
->driver($payment->client)
->setPaymentMethod($payment->gateway_type_id)
->processWebhookRequest($request->all(), $request->company(), $request->companyGateway(), $payment);
}
public function getTransactionReference(array $data, PaymentWebhookRequest $request)
{
$flatten = Arr::dot($data);
if (isset($flatten['data.object.id'])) {
return $flatten['data.object.id']; // stripe.com
}
if ($request->has('cko-session-id')) {
// checkout.com
}
return $request->getCompanyGateway()
->driver($request->getClient())
->processWebhookRequest($request, $request->getPayment());
}
}

View File

@ -1,4 +1,5 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
@ -9,12 +10,14 @@
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Requests\Payments;
use App\Http\Requests\Request;
use App\Models\Client;
use App\Models\Company;
use App\Models\CompanyGateway;
use App\Models\Payment;
use App\Models\PaymentHash;
class PaymentWebhookRequest extends Request
{
@ -30,28 +33,61 @@ class PaymentWebhookRequest extends Request
];
}
public function company()
/**
* Resolve company gateway.
*
* @param mixed $id
* @return null|\App\Models\CompanyGateway
*/
public function getCompanyGateway(): ?CompanyGateway
{
if (! $this->company_key) {
return false;
}
return Company::query()
->where('company_key', $this->company_key)
->firstOrFail();
return CompanyGateway::where('gateway_key', $this->gateway_key)->firstOrFail();
}
public function companyGateway()
/**
* Resolve payment hash.
*
* @param string $hash
* @return null|\App\Http\Requests\Payments\PaymentHash
*/
public function getPaymentHash(): ?PaymentHash
{
if (! $this->gateway_key || ! $this->company_key) {
return false;
if ($this->query('hash')) {
return PaymentHash::where('hash', $this->query('hash'))->firstOrFail();
}
}
$company = $this->company();
/**
* Resolve possible payment in the request.
*
* @return null|\App\Models\Payment
*/
public function getPayment(): ?Payment
{
$hash = $this->getPaymentHash();
return CompanyGateway::query()
->where('gateway_key', $this->gateway_key)
->where('company_id', $company->id)
->firstOrFail();
return $hash->payment;
}
/**
* Resolve client from payment hash.
*
* @return null|\App\Models\Client
*/
public function getClient(): ?Client
{
$hash = $this->getPaymentHash();
return Client::find($hash->data->client_id)->firstOrFail();
}
/**
* Resolve company from company_key parameter.
*
* @return null|\App\Models\Company
*/
public function getCompany(): ?Company
{
return Company::where('company_key', $this->company_key)->firstOrFail();
}
}

View File

@ -82,6 +82,7 @@ class CreditCard
'currency' => $request->currency,
'payment_hash' => $request->payment_hash,
'reference' => $request->payment_hash,
'client_id' => $this->checkout->client->id,
];
$state = array_merge($state, $request->all());
@ -121,8 +122,17 @@ class CreditCard
$payment->amount = $this->checkout->payment_hash->data->value;
$payment->reference = $this->checkout->payment_hash->data->reference;
$this->checkout->payment_hash->data = array_merge((array) $this->checkout->payment_hash->data, ['checkout_payment_ref' => $payment]);
$this->checkout->payment_hash->save();
if ($this->checkout->client->currency()->code === 'EUR') {
$payment->{'3ds'} = ['enabled' => true];
$payment->{'success_url'} = route('payment_webhook', [
'gateway_key' => $this->checkout->company_gateway->gateway_key,
'company_key' => $this->checkout->client->company->company_key,
'hash' => $this->checkout->payment_hash->hash,
]);
}
try {

View File

@ -29,6 +29,11 @@ trait Utilities
return $this->company_gateway->getConfigField('publicApiKey');
}
public function getParent()
{
return static::class == 'App\PaymentDrivers\CheckoutComPaymentDriver' ? $this : $this->checkout;
}
public function convertToCheckoutAmount($amount, $currency)
{
$cases = [
@ -52,42 +57,42 @@ trait Utilities
private function processSuccessfulPayment(Payment $_payment)
{
if ($this->checkout->payment_hash->data->store_card) {
if ($this->getParent()->payment_hash->data->store_card) {
$this->storePaymentMethod($_payment);
}
$data = [
'payment_method' => $_payment->source['id'],
'payment_type' => PaymentType::parseCardType(strtolower($_payment->source['scheme'])),
'amount' => $this->checkout->payment_hash->data->raw_value,
'amount' => $this->getParent()->payment_hash->data->raw_value,
'transaction_reference' => $_payment->id,
];
$payment = $this->checkout->createPayment($data, \App\Models\Payment::STATUS_COMPLETED);
$payment = $this->getParent()->createPayment($data, \App\Models\Payment::STATUS_COMPLETED);
SystemLogger::dispatch(
['response' => $_payment, 'data' => $data],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_SUCCESS,
SystemLog::TYPE_CHECKOUT,
$this->checkout->client
$this->getParent()->client
);
return redirect()->route('client.payments.show', ['payment' => $this->checkout->encodePrimaryKey($payment->id)]);
return redirect()->route('client.payments.show', ['payment' => $this->getParent()->encodePrimaryKey($payment->id)]);
}
public function processUnsuccessfulPayment(Payment $_payment)
{
PaymentFailureMailer::dispatch(
$this->checkout->client,
$this->getParent()->client,
$_payment,
$this->checkout->client->company,
$this->checkout->payment_hash->data->value
$this->getParent()->client->company,
$this->getParent()->payment_hash->data->value
);
$message = [
'server_response' => $_payment,
'data' => $this->checkout->payment_hash->data,
'data' => $this->getParent()->payment_hash->data,
];
SystemLogger::dispatch(
@ -95,7 +100,7 @@ trait Utilities
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_CHECKOUT,
$this->checkout->client
$this->getParent()->client
);
throw new PaymentFailed($_payment->status, $_payment->http_code);
@ -103,27 +108,10 @@ trait Utilities
private function processPendingPayment(Payment $_payment)
{
$data = [
'payment_method' => $_payment->source->id,
'payment_type' => PaymentType::CREDIT_CARD_OTHER,
'amount' => $this->checkout->payment_hash->data->value,
'transaction_reference' => $_payment->id,
];
$payment = $this->checkout->createPayment($data, \App\Models\Payment::STATUS_PENDING);
SystemLogger::dispatch(
['response' => $_payment, 'data' => $data],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_SUCCESS,
SystemLog::TYPE_CHECKOUT,
$this->checkout->client
);
try {
return redirect($_payment->_links['redirect']['href']);
} catch (Exception $e) {
return $this->processInternallyFailedPayment($this->checkout, $e);
return $this->processInternallyFailedPayment($this->getParent(), $e);
}
}
@ -140,10 +128,10 @@ trait Utilities
$data = [
'payment_meta' => $payment_meta,
'token' => $response->source['id'],
'payment_method_id' => $this->checkout->payment_hash->data->payment_method_id,
'payment_method_id' => $this->getParent()->payment_hash->data->payment_method_id,
];
return $this->checkout->storePaymentMethod($data);
return $this->getParent()->storePaymentMethod($data);
} catch (Exception $e) {
session()->flash('message', ctrans('texts.payment_method_saving_failed'));
}

View File

@ -12,7 +12,9 @@
namespace App\PaymentDrivers;
use App\Http\Requests\Payments\PaymentWebhookRequest;
use App\Models\ClientGatewayToken;
use App\Models\Company;
use App\Models\GatewayType;
use App\Models\Payment;
use App\Models\PaymentHash;
@ -23,6 +25,7 @@ use App\Utils\Traits\SystemLogTrait;
use Checkout\CheckoutApi;
use Checkout\Library\Exceptions\CheckoutHttpException;
use Checkout\Models\Payments\Refund;
use Exception;
class CheckoutComPaymentDriver extends BaseDriver
{
@ -134,7 +137,7 @@ class CheckoutComPaymentDriver extends BaseDriver
->route('client.profile.edit', ['client_contact' => auth()->user()->hashed_id])
->with('missing_required_fields', $this->required_fields);
}
return $this->payment_method->authorizeResponse($data);
}
@ -209,5 +212,24 @@ class CheckoutComPaymentDriver extends BaseDriver
public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash)
{
// ..
}
public function processWebhookRequest(PaymentWebhookRequest $request, Payment $payment = null)
{
$this->init();
$this->setPaymentHash($request->getPaymentHash());
try {
$payment = $this->gateway->payments()->details($request->query('cko-session-id'));
if ($payment->approved) {
return $this->processSuccessfulPayment($payment);
} else {
return $this->processUnsuccessfulPayment($payment);
}
} catch (CheckoutHttpException | Exception $e) {
return $this->processInternallyFailedPayment($this, $e);
}
}
}

View File

@ -3233,7 +3233,7 @@ return [
'test_pdf' => 'Test PDF',
'status_cancelled' => 'Cancelled',
'checkout_authorize_label' => 'Checkout.com can be can saved as payment method for future use, once you complete your first transaction. Don\'t forget to check "Save card" during payment process.',
'checkout_authorize_label' => 'Checkout.com can be can saved as payment method for future use, once you complete your first transaction. Don\'t forget to check "Store credit card details" during payment process.',
'node_status' => 'Node status',
'npm_status' => 'NPM status',

View File

@ -180,6 +180,6 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a
Route::post('support/messages/send', 'Support\Messages\SendingController');
});
Route::match(['get', 'post'], 'payment_webhook/{company_key?}/{gateway_key?}', 'PaymentWebhookController');
Route::match(['get', 'post'], 'payment_webhook/{gateway_key}/{company_key}', 'PaymentWebhookController')->name('payment_webhook');
Route::fallback('BaseController@notFound');