Skip to content
Snippets Groups Projects
Commit 19a0f6c8 authored by Vadym Abramchuk's avatar Vadym Abramchuk
Browse files

Issue #3270758 by abramm: Implement 3D Secure flow for new and existing credit cards

parent fe486656
Branches
Tags
1 merge request!6Issue #3270758: 3D Secure support
......@@ -2,8 +2,6 @@
namespace Drupal\commerce_omise;
use Drupal\commerce_order\Entity\OrderInterface;
use Drupal\commerce_payment\Entity\PaymentGatewayInterface;
use Drupal\commerce_payment\Exception\AuthenticationException;
use Drupal\commerce_payment\Exception\DeclineException;
use Drupal\commerce_payment\Exception\HardDeclineException;
......@@ -100,50 +98,4 @@ class OmiseUtil implements OmiseUtilInterface {
}
}
/**
* {@inheritdoc}
*/
public function getOrderPayment(OrderInterface $order) {
$payment_ids = $this
->paymentStorage
->getQuery()
->condition('order_id', $order->id())
->condition('payment_gateway', $this->getOmisePaymentGatewaysIds(), 'IN')
->execute();
if (empty($payment_ids)) {
// Throw error early if the query returned empty result.
throw new \InvalidArgumentException('No Omise payments found for order ID ' . $order->id());
}
$payments = $this
->paymentStorage
->loadMultiple($payment_ids);
if (count($payments) > 1) {
throw new \InvalidArgumentException('More than one Omise payment found for order ID ' . $order->id());
}
$payment = reset($payments);
if (!$payment) {
throw new \InvalidArgumentException('No Omise payments found for order ID ' . $order->id());
}
return $payment;
}
/**
* Returns list of IDs of all enabled Omise payment gateways.
*/
protected function getOmisePaymentGatewaysIds() {
$paymentGateways = array_filter(
$this->paymentGatewayStorage->loadMultiple(),
function (PaymentGatewayInterface $gateway) {
return $gateway->getPluginId() === 'omise' && $gateway->status();
});
return array_map(function (PaymentGatewayInterface $gateway) {
return $gateway->id();
}, $paymentGateways);
}
}
......@@ -2,7 +2,6 @@
namespace Drupal\commerce_omise;
use Drupal\commerce_order\Entity\OrderInterface;
use Drupal\commerce_price\Price;
/**
......@@ -43,18 +42,4 @@ interface OmiseUtilInterface {
*/
public function handleException(\OmiseException $exception);
/**
* Retrieves Omise payment from the order.
*
* @param \Drupal\commerce_order\Entity\OrderInterface $order
* The order entity.
*
* @return \Drupal\commerce_payment\Entity\PaymentInterface
* The Omise payment entity corresponding given order.
*
* @throws \InvalidArgumentException
* Thrown if there are no or more than one Omise payments for the order.
*/
public function getOrderPayment(OrderInterface $order);
}
......@@ -2,7 +2,9 @@
namespace Drupal\commerce_omise\Plugin\Commerce\PaymentGateway;
use Drupal\commerce\Response\NeedsRedirectException;
use Drupal\commerce_omise\OmiseUtilInterface;
use Drupal\commerce_order\Entity\OrderInterface;
use Drupal\commerce_payment\CreditCard;
use Drupal\commerce_payment\Entity\PaymentInterface;
use Drupal\commerce_payment\Entity\PaymentMethodInterface;
......@@ -14,7 +16,9 @@ use Drupal\commerce_price\Price;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Provides the Omise payment gateway.
......@@ -111,6 +115,7 @@ class Omise extends OnsitePaymentGatewayBase implements OmiseInterface {
return [
'secret_key' => '',
'public_key' => '',
'3d_secure' => FALSE,
] + parent::defaultConfiguration();
}
......@@ -134,6 +139,13 @@ class Omise extends OnsitePaymentGatewayBase implements OmiseInterface {
'#required' => TRUE,
];
$form['3d_secure'] = [
'#type' => 'checkbox',
'#title' => $this->t('Enable 3D Secure support'),
'#default_value' => !empty($this->configuration['3d_secure']),
'#required' => FALSE,
];
return $form;
}
......@@ -145,8 +157,9 @@ class Omise extends OnsitePaymentGatewayBase implements OmiseInterface {
if (!$form_state->getErrors()) {
$values = $form_state->getValue($form['#parents']);
$this->configuration['secret_key'] = $values['secret_key'];
$this->configuration['public_key'] = $values['public_key'];
$this->configuration['secret_key'] = $values['secret_key'];
$this->configuration['3d_secure'] = $values['3d_secure'];
}
}
......@@ -157,39 +170,69 @@ class Omise extends OnsitePaymentGatewayBase implements OmiseInterface {
$this->assertPaymentState($payment, ['new']);
$payment_method = $payment->getPaymentMethod();
$this->assertPaymentMethod($payment_method);
$amount = $payment->getAmount();
$currency_code = $payment->getAmount()->getCurrencyCode();
if ($payment->isNew()) {
// Save the payment entity to get its ID in case if it's not available
// yet.
$payment->save();
}
$transaction_data = [
'currency' => $currency_code,
'amount' => $this->util->formatNumber($amount),
'currency' => $payment->getAmount()->getCurrencyCode(),
'amount' => $this->util->formatNumber($payment->getAmount()),
'capture' => $capture,
// Always passing in a card since the user have selected or entered the
// specific card in Drupal user interface.
// Passing the 'customer' param without a card would always force using
// users default card stored at Omise regardless of what card has been
// selected in Drupal.
'card' => $payment_method->getRemoteId(),
];
$owner = $payment_method->getOwner();
if ($owner && $owner->isAuthenticated()) {
// Add remote customer ID if it's available; this is required for
// stored cards.
// @see doCreatePaymentMethod()
$transaction_data['customer'] = $this->getRemoteCustomerId($owner);
}
else {
$transaction_data['card'] = $payment_method->getRemoteId();
if (!empty($this->configuration['3d_secure'])) {
$transaction_data['return_uri'] = Url::fromRoute('commerce_payment.checkout.return', [
'commerce_order' => $payment->getOrderId(),
'step' => 'payment',
], [
'absolute' => TRUE,
'query' => [
'payment_id' => $payment->id(),
],
])->toString();
}
try {
$result = \OmiseCharge::create($transaction_data, $this->configuration['public_key'], $this->configuration['secret_key']);
if (!empty($result['status'] && $result['status'] === 'failed')) {
throw \OmiseException::getInstance([
'code' => $result['failure_code'],
'message' => $result['failure_message'],
]);
}
$result = \OmiseCharge::create(
$transaction_data,
$this->configuration['public_key'],
$this->configuration['secret_key']
);
$this->validateChargeStatus($result);
}
catch (\OmiseException $e) {
$this->util->handleException($e);
}
$payment->setRemoteId($result['id']);
if (!empty($result['authorize_uri'])) {
// Payment gateway requested redirecting user to their payment
// authorization page (e.g. for 3D Secure check).
// Store the payment (so the payment remote ID gets stored) and redirect
// user.
$payment->save();
throw new NeedsRedirectException($result['authorize_uri']);
}
$next_state = $capture ? 'completed' : 'authorization';
$payment->setState($next_state);
$payment->setRemoteId($result['id']);
$payment->save();
}
......@@ -337,6 +380,56 @@ class Omise extends OnsitePaymentGatewayBase implements OmiseInterface {
$payment_method->delete();
}
/**
* {@inheritdoc}
*/
public function updateRemotePaymentStatus(PaymentInterface $payment) {
$this->assertPaymentState($payment, ['new']);
try {
$charge = \OmiseCharge::retrieve(
$payment->getRemoteId(),
$this->configuration['public_key'],
$this->configuration['secret_key']
);
$this->validateChargeStatus($charge);
}
catch (\OmiseException $e) {
// Remove payment method from the order.
// Note we are NOT removing the payment method; it's still valid (since
// it passed the initial card number check), it's just not usable for this
// order (most likely due to invalid 3D Secure response provided by user).
// Removing the order payment method would force user re-entering (or
// selecting) it again.
$order = $payment->getOrder();
$order->get('payment_method')->setValue(NULL);
$order->save();
$this->util->handleException($e);
}
$payment->setState('completed');
$payment->save();
}
/**
* Validates Omise charge status.
*
* @param \OmiseCharge $charge
* The Omise charge object.
*
* @throws \OmiseException
* Omise errors are thrown if charge failed.
*/
protected function validateChargeStatus(\OmiseCharge $charge) {
if (!empty($charge['status'] && $charge['status'] === 'failed')) {
throw \OmiseException::getInstance([
'code' => $charge['failure_code'],
'message' => $charge['failure_message'],
]);
}
}
/**
* Creates the payment method on the gateway.
*
......@@ -406,9 +499,12 @@ class Omise extends OnsitePaymentGatewayBase implements OmiseInterface {
$this->setRemoteCustomerId($owner, $customer['id']);
$owner->save();
foreach ($cards['data'] as $card) {
return $card;
if (count($cards['data']) !== 1) {
throw new \RuntimeException(sprintf('Expected customer to have only 1 card, %d found.', count($cards['data'])));
}
return reset($cards['data']);
}
catch (\OmiseException $e) {
$this->util->handleException($e);
......@@ -428,4 +524,54 @@ class Omise extends OnsitePaymentGatewayBase implements OmiseInterface {
return [];
}
/**
* {@inheritdoc}
*/
public function getNotifyUrl() {
// This is a copy of OffsitePaymentGatewayBase::getNotifyUrl().
// Omise doesn't support usual Notify (i.e. when notify URL is passed in as
// part of the API call), however, there's a similar thing called Webhooks
// so keeping this code here for future reference.
return Url::fromRoute('commerce_payment.notify', [
'commerce_payment_gateway' => $this->parentEntity->id(),
], ['absolute' => TRUE]);
}
/**
* {@inheritdoc}
*/
public function onReturn(OrderInterface $order, Request $request) {
$payment_id = $request->query->get('payment_id');
if ($payment_id === NULL || !is_numeric($payment_id)) {
throw new \InvalidArgumentException('Missing/invalid payment ID query parameter.');
}
$payment = $this
->entityTypeManager
->getStorage('commerce_payment')
->load($payment_id);
if (!$payment) {
throw new \RuntimeException('The payment with given ID does not exist.');
}
$this->updateRemotePaymentStatus($payment);
}
/**
* {@inheritdoc}
*/
public function onCancel(OrderInterface $order, Request $request) {
$this->messenger()->addMessage($this->t('You have canceled checkout at @gateway but may resume the checkout process here when you are ready.', [
'@gateway' => $this->getDisplayLabel(),
]));
}
/**
* {@inheritdoc}
*/
public function onNotify(Request $request) {
// This may change later if we implement Webhooks support.
throw new \LogicException('Omise payment gateway does not support Notify method');
}
}
......@@ -2,6 +2,8 @@
namespace Drupal\commerce_omise\Plugin\Commerce\PaymentGateway;
use Drupal\commerce_payment\Entity\PaymentInterface;
use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\OffsitePaymentGatewayInterface;
use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\OnsitePaymentGatewayInterface;
use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsAuthorizationsInterface;
use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsRefundsInterface;
......@@ -9,7 +11,11 @@ use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsRefundsInterf
/**
* Provides the interface for the Omise payment gateway.
*/
interface OmiseInterface extends OnsitePaymentGatewayInterface, SupportsAuthorizationsInterface, SupportsRefundsInterface {
interface OmiseInterface extends
OnsitePaymentGatewayInterface,
SupportsAuthorizationsInterface,
SupportsRefundsInterface,
OffsitePaymentGatewayInterface {
/**
* Get the Omise API Public key set for the payment gateway.
......@@ -19,4 +25,15 @@ interface OmiseInterface extends OnsitePaymentGatewayInterface, SupportsAuthoriz
*/
public function getPublicKey();
/**
* Retrieve and update payment status for an existing charge.
*
* @param \Drupal\commerce_payment\Entity\PaymentInterface $payment
* The payment.
*
* @throws \Drupal\commerce_payment\Exception\PaymentGatewayException
* The Commerce exception.
*/
public function updateRemotePaymentStatus(PaymentInterface $payment);
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment