Skip to content

Commit 4250f83

Browse files
committed
Issue #2946909 by Sophie.SK, bojanz: When a payment gateway error occurs, the order does not become unocked
1 parent 8c1bfaa commit 4250f83

6 files changed

Lines changed: 113 additions & 42 deletions

File tree

modules/checkout/commerce_checkout.module

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
*/
77

88
use Drupal\commerce\EntityHelper;
9-
use Drupal\commerce_order\Entity\OrderInterface;
109
use Drupal\Core\Entity\EntityTypeInterface;
1110
use Drupal\Core\Field\BaseFieldDefinition;
1211
use Drupal\Core\Form\FormStateInterface;
@@ -118,26 +117,6 @@ function commerce_checkout_entity_base_field_info(EntityTypeInterface $entity_ty
118117
}
119118
}
120119

121-
/**
122-
* Implements hook_ENTITY_TYPE_presave().
123-
*/
124-
function commerce_checkout_commerce_order_presave(OrderInterface $order) {
125-
if (!$order->hasField('checkout_step')) {
126-
return;
127-
}
128-
129-
// Lock the order while on the 'payment' checkout step.
130-
$checkout_step = $order->get('checkout_step')->value;
131-
$original_order = $order->original;
132-
$previous_checkout_step = $original_order ? $original_order->get('checkout_step')->value : '';
133-
if ($checkout_step == 'payment' && $previous_checkout_step != 'payment') {
134-
$order->lock();
135-
}
136-
elseif ($checkout_step != 'payment' && $previous_checkout_step == 'payment') {
137-
$order->unlock();
138-
}
139-
}
140-
141120
/**
142121
* Implements hook_form_FORM_ID_alter() for 'commerce_order_type_form'.
143122
*/

modules/checkout/src/Plugin/Commerce/CheckoutFlow/CheckoutFlowBase.php

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,9 @@ public function getNextStepId($step_id) {
126126
*/
127127
public function redirectToStep($step_id) {
128128
$this->order->set('checkout_step', $step_id);
129-
if ($step_id == 'complete') {
130-
$transition = $this->order->getState()->getWorkflow()->getTransition('place');
131-
$this->order->getState()->applyTransition($transition);
132-
}
129+
$this->onStepChange($step_id);
133130
$this->order->save();
131+
134132
throw new NeedsRedirectException(Url::fromRoute('commerce_checkout.form', [
135133
'commerce_order' => $this->order->id(),
136134
'step' => $step_id,
@@ -291,21 +289,45 @@ public function validateForm(array &$form, FormStateInterface $form_state) {}
291289
public function submitForm(array &$form, FormStateInterface $form_state) {
292290
if ($next_step_id = $this->getNextStepId($form['#step_id'])) {
293291
$this->order->set('checkout_step', $next_step_id);
292+
$this->onStepChange($next_step_id);
293+
294294
$form_state->setRedirect('commerce_checkout.form', [
295295
'commerce_order' => $this->order->id(),
296296
'step' => $next_step_id,
297297
]);
298-
299-
if ($next_step_id == 'complete') {
300-
// Place the order.
301-
$transition = $this->order->getState()->getWorkflow()->getTransition('place');
302-
$this->order->getState()->applyTransition($transition);
303-
}
304298
}
305299

306300
$this->order->save();
307301
}
308302

303+
/**
304+
* Reacts to the current step changing.
305+
*
306+
* Called before saving the order and redirecting.
307+
*
308+
* Handles the following logic
309+
* 1) Locks the order before the payment page,
310+
* 2) Unlocks the order when leaving the payment page
311+
* 3) Places the order before the complete page.
312+
*
313+
* @param string $step_id
314+
* The new step ID.
315+
*/
316+
protected function onStepChange($step_id) {
317+
// Lock the order while on the 'payment' checkout step. Unlock elsewhere.
318+
if ($step_id == 'payment') {
319+
$this->order->lock();
320+
}
321+
elseif ($step_id != 'payment') {
322+
$this->order->unlock();
323+
}
324+
// Place the order.
325+
if ($step_id == 'complete') {
326+
$transition = $this->order->getState()->getWorkflow()->getTransition('place');
327+
$this->order->getState()->applyTransition($transition);
328+
}
329+
}
330+
309331
/**
310332
* Gets whether the given step has a sidebar.
311333
*

modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentProcess.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
namespace Drupal\commerce_payment\Plugin\Commerce\CheckoutPane;
44

5-
use Drupal\commerce\Response\NeedsRedirectException;
65
use Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowInterface;
76
use Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\CheckoutPaneBase;
87
use Drupal\commerce_payment\Exception\DeclineException;
@@ -200,8 +199,8 @@ public function buildPaneForm(array $pane_form, FormStateInterface $form_state,
200199
$complete_form['actions']['next']['#value'] = $this->t('Proceed to @gateway', [
201200
'@gateway' => $payment_gateway_plugin->getDisplayLabel(),
202201
]);
203-
// The 'Go back' link needs to use the cancel URL to ensure that the
204-
// order is unlocked when the customer is sent to the previous page.
202+
// Make sure that the payment gateway's onCancel() method is invoked,
203+
// by pointing the "Go back" link to the cancel URL.
205204
$complete_form['actions']['next']['#suffix'] = Link::fromTextAndUrl($this->t('Go back'), $this->buildCancelUrl())->toString();
206205
// Hide the actions by default, they are not needed by gateways that
207206
// embed iframes or redirect via GET. The offsite-payment form can
@@ -272,7 +271,8 @@ protected function buildPaymentInformationStepUrl() {
272271
* @throws \Drupal\commerce\Response\NeedsRedirectException
273272
*/
274273
protected function redirectToPreviousStep() {
275-
throw new NeedsRedirectException($this->buildPaymentInformationStepUrl()->toString());
274+
$step_id = $this->checkoutFlow->getPane('payment_information')->getStepId();
275+
return $this->checkoutFlow->redirectToStep($step_id);
276276
}
277277

278278
}

modules/payment/tests/src/FunctionalJavascript/PaymentCheckoutTest.php

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ public function testCheckoutWithExistingPaymentMethod() {
261261
$order = Order::load(1);
262262
$this->assertEquals('onsite', $order->get('payment_gateway')->target_id);
263263
$this->assertEquals('1', $order->get('payment_method')->target_id);
264-
264+
$this->assertFalse($order->isLocked());
265265
// Verify that a payment was created.
266266
$payment = Payment::load(1);
267267
$this->assertNotNull($payment);
@@ -315,7 +315,7 @@ public function testCheckoutWithNewPaymentMethod() {
315315
$payment_method = $order->get('payment_method')->entity;
316316
$this->assertEquals('1881', $payment_method->get('card_number')->value);
317317
$this->assertEquals('123 New York Drive', $payment_method->getBillingProfile()->get('address')->address_line1);
318-
318+
$this->assertFalse($order->isLocked());
319319
// Verify that a payment was created.
320320
$payment = Payment::load(1);
321321
$this->assertNotNull($payment);
@@ -354,6 +354,8 @@ public function testCheckoutWithDeclinedPaymentMethod() {
354354
$this->assertSession()->pageTextContains('We encountered an error processing your payment method. Please verify your details and try again.');
355355
$this->assertSession()->addressEquals('checkout/1/order_information');
356356

357+
$order = Order::load(1);
358+
$this->assertFalse($order->isLocked());
357359
// Verify a payment was not created.
358360
$payment = Payment::load(1);
359361
$this->assertNull($payment);
@@ -384,9 +386,63 @@ public function testCheckoutWithOffsiteRedirectPost() {
384386
$this->assertSession()->pageTextContains('123 New York Drive');
385387
$this->submitForm([], 'Pay and complete purchase');
386388
$this->assertSession()->pageTextContains('Your order number is 1. You can view your order on your account page when logged in.');
389+
387390
$order = Order::load(1);
388391
$this->assertEquals('offsite', $order->get('payment_gateway')->target_id);
392+
$this->assertFalse($order->isLocked());
393+
// Verify that a payment was created.
394+
$payment = Payment::load(1);
395+
$this->assertNotNull($payment);
396+
$this->assertEquals($payment->getAmount(), $order->getTotalPrice());
397+
}
398+
399+
/**
400+
* Tests checkout with an off-site gateway (POST redirect method, manual).
401+
*
402+
* In this scenario the customer must click the submit button on the payment
403+
* page in order to proceed to the gateway.
404+
*/
405+
public function testCheckoutWithOffsiteRedirectPostManual() {
406+
$payment_gateway = PaymentGateway::load('offsite');
407+
$payment_gateway->getPlugin()->setConfiguration([
408+
'redirect_method' => 'post_manual',
409+
'payment_method_types' => ['credit_card'],
410+
]);
411+
$payment_gateway->save();
389412

413+
$this->drupalGet($this->product->toUrl()->toString());
414+
$this->submitForm([], 'Add to cart');
415+
$this->drupalGet('checkout/1');
416+
$radio_button = $this->getSession()->getPage()->findField('Example');
417+
$radio_button->click();
418+
$this->waitForAjaxToFinish();
419+
420+
$this->submitForm([
421+
'payment_information[billing_information][address][0][address][given_name]' => 'Johnny',
422+
'payment_information[billing_information][address][0][address][family_name]' => 'Appleseed',
423+
'payment_information[billing_information][address][0][address][address_line1]' => '123 New York Drive',
424+
'payment_information[billing_information][address][0][address][locality]' => 'New York City',
425+
'payment_information[billing_information][address][0][address][administrative_area]' => 'NY',
426+
'payment_information[billing_information][address][0][address][postal_code]' => '10001',
427+
], 'Continue to review');
428+
$this->assertSession()->pageTextContains('Payment information');
429+
$this->assertSession()->pageTextContains('Example');
430+
$this->assertSession()->pageTextContains('Johnny Appleseed');
431+
$this->assertSession()->pageTextContains('123 New York Drive');
432+
$this->submitForm([], 'Pay and complete purchase');
433+
434+
$this->assertSession()->addressEquals('checkout/1/payment');
435+
$order = Order::load(1);
436+
$this->assertEquals('offsite', $order->get('payment_gateway')->target_id);
437+
$this->assertTrue($order->isLocked());
438+
439+
$this->submitForm([], 'Proceed to Example');
440+
$this->assertSession()->pageTextContains('Your order number is 1. You can view your order on your account page when logged in.');
441+
442+
\Drupal::entityTypeManager()->getStorage('commerce_order')->resetCache(['1']);
443+
$order = Order::load(1);
444+
$this->assertEquals('offsite', $order->get('payment_gateway')->target_id);
445+
$this->assertFalse($order->isLocked());
390446
// Verify that a payment was created.
391447
$payment = Payment::load(1);
392448
$this->assertNotNull($payment);
@@ -431,9 +487,10 @@ public function testCheckoutWithOffsiteRedirectGet() {
431487
$this->assertSession()->pageTextContains('123 New York Drive');
432488
$this->submitForm([], 'Pay and complete purchase');
433489
$this->assertSession()->pageTextContains('Your order number is 1. You can view your order on your account page when logged in.');
490+
434491
$order = Order::load(1);
435492
$this->assertEquals('offsite', $order->get('payment_gateway')->target_id);
436-
493+
$this->assertFalse($order->isLocked());
437494
// Verify that a payment was created.
438495
$payment = Payment::load(1);
439496
$this->assertNotNull($payment);
@@ -477,6 +534,9 @@ public function testFailedCheckoutWithOffsiteRedirectGet() {
477534
$this->assertSession()->pageTextContains('We encountered an unexpected error processing your payment. Please try again later.');
478535
$this->assertSession()->addressEquals('checkout/1/order_information');
479536

537+
$order = Order::load(1);
538+
// @todo Fix order unlocking in this situation.
539+
// $this->assertFalse($order->isLocked());
480540
// Verify a payment was not created.
481541
$payment = Payment::load(1);
482542
$this->assertNull($payment);
@@ -508,9 +568,10 @@ public function testCheckoutWithManual() {
508568
$this->submitForm([], 'Pay and complete purchase');
509569
$this->assertSession()->pageTextContains('Your order number is 1. You can view your order on your account page when logged in.');
510570
$this->assertSession()->pageTextContains('Sample payment instructions.');
571+
511572
$order = Order::load(1);
512573
$this->assertEquals('manual', $order->get('payment_gateway')->target_id);
513-
574+
$this->assertFalse($order->isLocked());
514575
// Verify that a payment was created.
515576
$payment = Payment::load(1);
516577
$this->assertNotNull($payment);

modules/payment_example/src/Plugin/Commerce/PaymentGateway/OffsiteRedirect.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
4747
'#title' => $this->t('Redirect method'),
4848
'#options' => [
4949
'get' => $this->t('Redirect via GET (302 header)'),
50-
'post' => $this->t('Redirect via POST'),
50+
'post' => $this->t('Redirect via POST (automatic)'),
51+
'post_manual' => $this->t('Redirect via POST (manual)'),
5152
],
5253
'#default_value' => $this->configuration['redirect_method'],
5354
];

modules/payment_example/src/PluginForm/OffsiteRedirect/PaymentOffsiteForm.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
2020
/** @var \Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\OffsitePaymentGatewayInterface $payment_gateway_plugin */
2121
$payment_gateway_plugin = $payment->getPaymentGateway()->getPlugin();
2222
$redirect_method = $payment_gateway_plugin->getConfiguration()['redirect_method'];
23-
if ($redirect_method == 'post') {
23+
$remove_js = ($redirect_method == 'post_manual');
24+
if (in_array($redirect_method, ['post', 'post_manual'])) {
2425
$redirect_url = Url::fromRoute('commerce_payment_example.dummy_redirect_post')->toString();
26+
$redirect_method = 'post';
2527
}
2628
else {
2729
// Gateways that use the GET redirect method usually perform an API call
@@ -44,7 +46,13 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
4446
'total' => $payment->getAmount()->getNumber(),
4547
];
4648

47-
return $this->buildRedirectForm($form, $form_state, $redirect_url, $data, $redirect_method);
49+
$form = $this->buildRedirectForm($form, $form_state, $redirect_url, $data, $redirect_method);
50+
if ($remove_js) {
51+
// Disable the javascript that auto-clicks the Submit button.
52+
unset($form['#attached']['library']);
53+
}
54+
55+
return $form;
4856
}
4957

5058
}

0 commit comments

Comments
 (0)