diff --git a/modules/payment/commerce_payment.routing.yml b/modules/payment/commerce_payment.routing.yml index f6961460..cffc559a 100644 --- a/modules/payment/commerce_payment.routing.yml +++ b/modules/payment/commerce_payment.routing.yml @@ -105,7 +105,7 @@ commerce_payment.notify: commerce_payment.offsite_payment_method.return: path: '/user/{user}/payment-methods/{payment_gateway}/return' defaults: - _controller: '\Drupal\commerce_payment\Controller\OffsitePaymentMethodUserPageController::returnPage' + _controller: '\Drupal\commerce_payment\Controller\PaymentUserController::paymentMethodReturn' requirements: _custom_access: '\Drupal\commerce_payment\Access\PaymentMethodAccessCheck::checkAccess' options: @@ -118,7 +118,7 @@ commerce_payment.offsite_payment_method.return: commerce_payment.offsite_payment_method.cancel: path: '/user/{user}/payment-methods/{payment_gateway}/cancel' defaults: - _controller: '\Drupal\commerce_payment\Controller\OffsitePaymentMethodUserPageController::cancelPage' + _controller: '\Drupal\commerce_payment\Controller\PaymentUserController::paymentMethodCancel' requirements: _custom_access: '\Drupal\commerce_payment\Access\PaymentMethodAccessCheck::checkAccess' options: diff --git a/modules/payment/src/Controller/PaymentCheckoutController.php b/modules/payment/src/Controller/PaymentCheckoutController.php index b684f4cc..7fd5556a 100644 --- a/modules/payment/src/Controller/PaymentCheckoutController.php +++ b/modules/payment/src/Controller/PaymentCheckoutController.php @@ -107,38 +107,6 @@ class PaymentCheckoutController implements ContainerInjectionInterface { $checkout_flow = $order->get('checkout_flow')->entity; $checkout_flow_plugin = $checkout_flow->getPlugin(); - if ($payment_gateway_plugin instanceof SupportsStoredPaymentMethodsInterface) { - try { - $payment_method_storage = $this->entityTypeManager->getStorage('commerce_payment_method'); - /** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method */ - $payment_method = $payment_method_storage->create([ - // Payment method type may depend on the request payload. We create - // payment method stub based on default payment method type available - // for payment gateway plugin but module developers can swap it - // with custom payment method. Keep in mind that $payment_method is - // passed by reference and code below relies on that reference to add - // payment method to the order. - 'type' => $payment_gateway_plugin->getDefaultPaymentMethodType()->getPluginId(), - 'payment_gateway' => $payment_gateway, - 'uid' => $order->getCustomerId(), - 'billing_profile' => $order->getBillingProfile(), - ]); - // In case payment gateway supports payment method. - $payment_details = $request->isMethod('GET') ? $request->query->all() : $request->request->all(); - $payment_gateway_plugin->createPaymentMethod($payment_method, $payment_details); - - // Add payment method to the order. - $order->set('payment_method', $payment_method); - $order->save(); - } - catch (PaymentGatewayException $e) { - // If creating payment method failed we allow onReturn to handle - // creating Payment, which is required to fulfill the payment. This - // exception is muted and logged. - $this->logger->error($e->getMessage()); - } - } - try { $payment_gateway_plugin->onReturn($order, $request); $redirect_step_id = $checkout_flow_plugin->getNextStepId($step_id); diff --git a/modules/payment/src/Controller/OffsitePaymentMethodUserPageController.php b/modules/payment/src/Controller/PaymentUserController.php similarity index 72% rename from modules/payment/src/Controller/OffsitePaymentMethodUserPageController.php rename to modules/payment/src/Controller/PaymentUserController.php index 06d2ea01..c8ffd34e 100644 --- a/modules/payment/src/Controller/OffsitePaymentMethodUserPageController.php +++ b/modules/payment/src/Controller/PaymentUserController.php @@ -4,7 +4,8 @@ namespace Drupal\commerce_payment\Controller; use Drupal\commerce_payment\Entity\PaymentGatewayInterface; use Drupal\commerce_payment\Exception\PaymentGatewayException; -use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsCreatingOffsitePaymentMethodsOnUserPageInterface; +use Drupal\commerce_payment\PaymentMethodStorageInterface; +use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsCreatingOffsitePaymentMethodsInterface; use Drupal\Core\Access\AccessException; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; @@ -22,7 +23,7 @@ use Symfony\Component\HttpFoundation\Request; /** * Provides endpoints for off-site payment methods. */ -class OffsitePaymentMethodUserPageController implements ContainerInjectionInterface { +class PaymentUserController implements ContainerInjectionInterface { use StringTranslationTrait; @@ -101,30 +102,20 @@ class OffsitePaymentMethodUserPageController implements ContainerInjectionInterf * * @return \Symfony\Component\HttpFoundation\RedirectResponse */ - public function returnPage(PaymentGatewayInterface $payment_gateway, AccountInterface $user, Request $request) { - /** @var \Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsCreatingOffsitePaymentMethodsOnUserPageInterface $payment_gateway_plugin */ + public function paymentMethodReturn(PaymentGatewayInterface $payment_gateway, AccountInterface $user, Request $request) { + /** @var \Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsCreatingOffsitePaymentMethodsInterface $payment_gateway_plugin */ $payment_gateway_plugin = $payment_gateway->getPlugin(); - if (!$payment_gateway_plugin instanceof SupportsCreatingOffsitePaymentMethodsOnUserPageInterface) { - throw new AccessException('The payment gateway for the order does not implement ' . SupportsCreatingOffsitePaymentMethodsOnUserPageInterface::class); + if (!$payment_gateway_plugin instanceof SupportsCreatingOffsitePaymentMethodsInterface) { + throw new AccessException('The payment gateway for the order does not implement ' . SupportsCreatingOffsitePaymentMethodsInterface::class); } try { $payment_method_storage = $this->entityTypeManager->getStorage('commerce_payment_method'); - /** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method */ - $payment_method = $payment_method_storage->create([ - // Payment method type may depend on the request payload. We create - // payment method stub based on default payment method type available - // for payment gateway plugin but module developers can swap it - // with custom payment method. Keep in mind that $payment_method is - // passed by reference and code below relies on that reference to add - // payment method to the order. - 'type' => $payment_gateway_plugin->getDefaultPaymentMethodType()->getPluginId(), - 'payment_gateway' => $payment_gateway, - 'uid' => $user->id(), - 'payment_gateway_mode' => $payment_gateway_plugin->getMode(), - ]); - // In case payment gateway supports payment method. - $payment_details = $request->isMethod('GET') ? $request->query->all() : $request->request->all(); - $payment_gateway_plugin->createPaymentMethod($payment_method, $payment_details); + $payment_method = $payment_method_storage->createForCustomer( + $payment_gateway_plugin->getDefaultPaymentMethodType()->getPluginId(), + $payment_gateway, + $user->id() + ); + $payment_gateway_plugin->createPaymentMethod($payment_method, $request); return new RedirectResponse(Url::fromRoute('entity.commerce_payment_method.collection', ['user' => $user->id()])->toString()); } catch (PaymentGatewayException $e) { @@ -146,12 +137,12 @@ class OffsitePaymentMethodUserPageController implements ContainerInjectionInterf * * @return \Symfony\Component\HttpFoundation\RedirectResponse */ - public function cancelPage(PaymentGatewayInterface $payment_gateway, AccountInterface $user, Request $request) { + public function paymentMethodCancel(PaymentGatewayInterface $payment_gateway, AccountInterface $user, Request $request) { $payment_gateway_plugin = $payment_gateway->getPlugin(); - if (!$payment_gateway_plugin instanceof SupportsCreatingOffsitePaymentMethodsOnUserPageInterface) { - throw new AccessException('The payment gateway for the order does not implement ' . SupportsCreatingOffsitePaymentMethodsOnUserPageInterface::class); + if (!$payment_gateway_plugin instanceof SupportsCreatingOffsitePaymentMethodsInterface) { + throw new AccessException('The payment gateway for the order does not implement ' . SupportsCreatingOffsitePaymentMethodsInterface::class); } - $payment_gateway_plugin->onCancelOffsitePaymentMethodOnUserPage($request); + $payment_gateway_plugin->cancelCreatePaymentMethod($request); return new RedirectResponse(Url::fromRoute('entity.commerce_payment_method.collection', ['user' => $user->id()])->toString()); } diff --git a/modules/payment/src/Form/PaymentMethodAddForm.php b/modules/payment/src/Form/PaymentMethodAddForm.php index 09c9d212..8b2a60a7 100644 --- a/modules/payment/src/Form/PaymentMethodAddForm.php +++ b/modules/payment/src/Form/PaymentMethodAddForm.php @@ -3,7 +3,8 @@ namespace Drupal\commerce_payment\Form; use Drupal\commerce\InlineFormManager; -use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsCreatingOffsitePaymentMethodsOnUserPageInterface; +use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsCreatingOffsitePaymentMethodsInterface; +use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsCreatingPaymentMethodsInterface; use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsStoredPaymentMethodsInterface; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; @@ -72,8 +73,12 @@ class PaymentMethodAddForm extends FormBase implements ContainerInjectionInterfa /** @var \Drupal\commerce_payment\PaymentGatewayStorageInterface $payment_gateway_storage */ $payment_gateway_storage = $this->entityTypeManager->getStorage('commerce_payment_gateway'); $payment_gateway = $payment_gateway_storage->loadForUser($user); + if (!$payment_gateway) { + throw new AccessDeniedHttpException(); + } + $payment_gateway_plugin = $payment_gateway->getPlugin(); // @todo Move this check to the access handler. - if (!$payment_gateway || (!($payment_gateway->getPlugin() instanceof SupportsStoredPaymentMethodsInterface) && !($payment_gateway->getPlugin() instanceof SupportsCreatingOffsitePaymentMethodsOnUserPageInterface))) { + if (!$payment_gateway_plugin instanceof SupportsCreatingPaymentMethodsInterface && !$payment_gateway_plugin instanceof SupportsCreatingOffsitePaymentMethodsInterface) { throw new AccessDeniedHttpException(); } $form_state->set('payment_gateway', $payment_gateway); @@ -164,7 +169,7 @@ class PaymentMethodAddForm extends FormBase implements ContainerInjectionInterfa '#parents' => ['payment_method'], '#inline_form' => $inline_form, ]; - if ($payment_gateway && $payment_gateway->getPlugin() instanceof SupportsCreatingOffsitePaymentMethodsOnUserPageInterface) { + if ($payment_gateway && $payment_gateway->getPlugin() instanceof SupportsCreatingOffsitePaymentMethodsInterface) { $form['payment_method']['#return_url'] = $this->buildReturnUrl($form_state->getBuildInfo()['args'][0]->id(), $payment_gateway->id())->toString(); $form['payment_method']['#cancel_url'] = $this->buildCancelUrl($form_state->getBuildInfo()['args'][0]->id(), $payment_gateway->id())->toString(); } diff --git a/modules/payment/src/PaymentGatewayStorage.php b/modules/payment/src/PaymentGatewayStorage.php index 2fe968a6..eb535187 100644 --- a/modules/payment/src/PaymentGatewayStorage.php +++ b/modules/payment/src/PaymentGatewayStorage.php @@ -5,7 +5,7 @@ namespace Drupal\commerce_payment; use Drupal\commerce_order\Entity\OrderInterface; use Drupal\commerce_payment\Event\FilterPaymentGatewaysEvent; use Drupal\commerce_payment\Event\PaymentEvents; -use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsCreatingOffsitePaymentMethodsOnUserPageInterface; +use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsCreatingOffsitePaymentMethodsInterface; use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsStoredPaymentMethodsInterface; use Drupal\Component\Uuid\UuidInterface; use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface; @@ -71,7 +71,7 @@ class PaymentGatewayStorage extends ConfigEntityStorage implements PaymentGatewa public function loadForUser(UserInterface $account) { $payment_gateways = $this->loadByProperties(['status' => TRUE]); $payment_gateways = array_filter($payment_gateways, function ($payment_gateway) { - return $payment_gateway->getPlugin() instanceof SupportsStoredPaymentMethodsInterface || $payment_gateway->getPlugin() instanceof SupportsCreatingOffsitePaymentMethodsOnUserPageInterface; + return $payment_gateway->getPlugin() instanceof SupportsStoredPaymentMethodsInterface; }); // @todo Implement resolving logic. $payment_gateway = reset($payment_gateways); diff --git a/modules/payment/src/PaymentMethodStorage.php b/modules/payment/src/PaymentMethodStorage.php index 6b39df04..9592cfb6 100644 --- a/modules/payment/src/PaymentMethodStorage.php +++ b/modules/payment/src/PaymentMethodStorage.php @@ -14,6 +14,7 @@ use Drupal\Core\Entity\EntityTypeBundleInfoInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\profile\Entity\ProfileInterface; use Drupal\user\UserInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -125,4 +126,17 @@ class PaymentMethodStorage extends CommerceContentEntityStorage implements Payme return $payment_methods; } + + /** + * {@inheritdoc} + */ + public function createForCustomer($payment_method_type, $payment_gateway_id, $customer_id, ProfileInterface $billing_profile = NULL) { + return $this->create([ + 'type' => $payment_method_type, + 'payment_gateway' => $payment_gateway_id, + 'uid' => $customer_id, + 'billing_profile' => $billing_profile, + ]); + } + } diff --git a/modules/payment/src/PaymentMethodStorageInterface.php b/modules/payment/src/PaymentMethodStorageInterface.php index ff95f9ca..04fcbc55 100644 --- a/modules/payment/src/PaymentMethodStorageInterface.php +++ b/modules/payment/src/PaymentMethodStorageInterface.php @@ -4,6 +4,7 @@ namespace Drupal\commerce_payment; use Drupal\commerce_payment\Entity\PaymentGatewayInterface; use Drupal\Core\Entity\ContentEntityStorageInterface; +use Drupal\profile\Entity\ProfileInterface; use Drupal\user\UserInterface; /** @@ -30,4 +31,22 @@ interface PaymentMethodStorageInterface extends ContentEntityStorageInterface { */ public function loadReusable(UserInterface $account, PaymentGatewayInterface $payment_gateway, array $billing_countries = []); + + /** + * Constructs a payment method for a customer, without permanently saving it. + * + * @param string $payment_method_type + * The payment method type. + * @param string $payment_gateway_id + * The payment gateway ID. + * @param string|int $customer_id + * The customer ID. + * @param \Drupal\profile\Entity\ProfileInterface $billing_profile + * The billing profile, optional. + * + * @return \Drupal\commerce_payment\Entity\PaymentMethodInterface + * A new payment method object. + */ + public function createForCustomer($payment_method_type, $payment_gateway_id, $customer_id, ProfileInterface $billing_profile = NULL); + } diff --git a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php index 346cc501..ad2a0f8e 100644 --- a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php +++ b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php @@ -271,12 +271,12 @@ class PaymentInformation extends CheckoutPaneBase { /** @var \Drupal\commerce_payment\PaymentMethodStorageInterface $payment_method_storage */ $payment_method_storage = $this->entityTypeManager->getStorage('commerce_payment_method'); - $payment_method = $payment_method_storage->create([ - 'type' => $payment_option->getPaymentMethodTypeId(), - 'payment_gateway' => $payment_option->getPaymentGatewayId(), - 'uid' => $this->order->getCustomerId(), - 'billing_profile' => $this->order->getBillingProfile(), - ]); + $payment_method = $payment_method_storage->createForCustomer( + $payment_option->getPaymentMethodTypeId(), + $payment_option->getPaymentGatewayId(), + $this->order->getCustomerId(), + $this->order->getBillingProfile() + ); $inline_form = $this->inlineFormManager->createInstance('payment_gateway_form', [ 'operation' => 'add-payment-method', ], $payment_method); diff --git a/modules/payment/src/Plugin/Commerce/PaymentGateway/SupportsCreatingOffsitePaymentMethodsInterface.php b/modules/payment/src/Plugin/Commerce/PaymentGateway/SupportsCreatingOffsitePaymentMethodsInterface.php new file mode 100644 index 00000000..83eeba6f --- /dev/null +++ b/modules/payment/src/Plugin/Commerce/PaymentGateway/SupportsCreatingOffsitePaymentMethodsInterface.php @@ -0,0 +1,39 @@ +isMethod('GET') ? $request->query->all() : $request->request->all(); + $required_keys = ['cardprefix', 'cardnomask', 'cardexpdate']; + foreach ($required_keys as $required_key) { + if (empty($payment_details[$required_key])) { + throw new \InvalidArgumentException(sprintf('$payment_details must contain the %s key.', $required_key)); + } + } + + if ($billing_profile = $payment_method->getBillingProfile()) { + /** @var \Drupal\address\Plugin\Field\FieldType\AddressItem $billing_address */ + $billing_address = $billing_profile->get('address')->first(); + if ($billing_address->getPostalCode() == '53141') { + throw new HardDeclineException('The payment method was declined'); + } + } + $payment_method->card_type = CreditCard::detectType($payment_details['cardprefix'])->getId(); + // Only the last 4 numbers are safe to store. $payment_method->card_number = substr($payment_details['cardnomask'], -4); $payment_method->card_exp_month = substr($payment_details['cardexpdate'], 0, 2); $payment_method->card_exp_year = substr($payment_details['cardexpdate'], -2); - - $payment_method->setRemoteId($payment_details['txn_id']); $expires = CreditCard::calculateExpirationTimestamp($payment_method->card_exp_month->value, $payment_method->card_exp_year->value); $payment_method->setExpiresTime($expires); + // The remote ID returned by the request. + $payment_method->setRemoteId($payment_details['remote_payment_method_id']); $payment_method->save(); } + /** + * {@inheritdoc} + */ + public function cancelCreatePaymentMethod(Request $request) { + $this->messenger->addWarning($this->t('Payment method creation was canceled.')); + } + /** * {@inheritdoc} */ @@ -136,9 +161,15 @@ class StoredOffsiteRedirect extends OffsiteRedirect implements SupportsStoredPay */ public function onReturn(OrderInterface $order, Request $request) { // @todo Add examples of request validation. - // Note: Since requires_billing_information is FALSE, the order is - // not guaranteed to have a billing profile. Confirm that - // $order->getBillingProfile() is not NULL before trying to use it. + $payment_method_storage = $this->entityTypeManager->getStorage('commerce_payment_method'); + assert($payment_method_storage instanceof PaymentMethodStorageInterface); + $payment_method = $payment_method_storage->createForCustomer( + $this->getDefaultPaymentMethodType(), + $this->parentEntity->id(), + $order->getCustomerId(), + $order->getBillingProfile() + ); + $this->createPaymentMethod($payment_method, $request); $payment_storage = $this->entityTypeManager->getStorage('commerce_payment'); $payment = $payment_storage->create([ 'state' => 'completed', @@ -147,62 +178,9 @@ class StoredOffsiteRedirect extends OffsiteRedirect implements SupportsStoredPay 'order_id' => $order->id(), 'remote_id' => $request->query->get('txn_id'), 'remote_state' => $request->query->get('payment_status'), - 'payment_method' => $order->get('payment_method')->target_id, + 'payment_method' => $payment_method, ]); $payment->save(); } - /** - * {@inheritdoc} - */ - public function onReturnOffsitePaymentMethodOnUserPage(PaymentMethodInterface $payment_method, array $payment_details) { - // This is the exact copy of - // Drupal\commerce_payment_example\Plugin\Commerce\PaymentGateway\Onsite::createPaymentMethodOnUserPage(). - $required_keys = [ - // The expected keys are payment gateway specific and usually match - // the PaymentMethodAddForm form elements. They are expected to be valid. - 'cardprefix', 'cardnomask', 'cardexpdate', - ]; - foreach ($required_keys as $required_key) { - if (empty($payment_details[$required_key])) { - throw new \InvalidArgumentException(sprintf('$payment_details must contain the %s key.', $required_key)); - } - } - // Add a built in test for testing decline exceptions. - // Note: Since requires_billing_information is FALSE, the payment method - // is not guaranteed to have a billing profile. Confirm tha - // $payment_method->getBillingProfile() is not NULL before trying to use it. - if ($billing_profile = $payment_method->getBillingProfile()) { - /** @var \Drupal\address\Plugin\Field\FieldType\AddressItem $billing_address */ - $billing_address = $billing_profile->get('address')->first(); - if ($billing_address->getPostalCode() == '53141') { - throw new HardDeclineException('The payment method was declined'); - } - } - - // Perform the create request here, throw an exception if it fails. - // See \Drupal\commerce_payment\Exception for the available exceptions. - // You might need to do different API requests based on whether the - // payment method is reusable: $payment_method->isReusable(). - // Non-reusable payment methods usually have an expiration timestamp. - $payment_method->card_type = CreditCard::detectType($payment_details['cardprefix'])->getId(); - // Only the last 4 numbers are safe to store. - $payment_method->card_number = substr($payment_details['cardnomask'], -4); - $payment_method->card_exp_month = substr($payment_details['cardexpdate'], 0, 2); - $payment_method->card_exp_year = substr($payment_details['cardexpdate'], -2); - $expires = CreditCard::calculateExpirationTimestamp($payment_method->card_exp_month->value, $payment_method->card_exp_year->value); - $payment_method->setExpiresTime($expires); - // The remote ID returned by the request. - $payment_method->setRemoteId($payment_details['remote_payment_method_id']); - $payment_method->save(); - $this->messenger->addMessage($this->t('A new payment method has been created.')); - } - - /** - * {@inheritdoc} - */ - public function onCancelOffsitePaymentMethodOnUserPage(Request $request) { - $this->messenger->addWarning($this->t('Payment method creation was canceled.')); - } - }