I'm new to Drupal and have to make a Offsite Payment Gateway.an off-site payment gateway (Withwith Drupal Commerce 2)
. It all works - except sometimes it doesn't.
I believe this is due to a race condition between onReturn()
and onNotify()
: In onNotify()
i set a property on the order and save the order. However, Drupal Commerce seems to automatically save the order in onReturn()
too (out of my control). I think thatbut sometimes this saving of the order in onReturn()
will overwrite the change i did to the order in onNotify(), causing the data to be lost, when onReturn()
and onNotify()
are being called at almost the same time.
Is there a safe way to do ->setData(...)
on the $order
in onNotify()
? (How would you do it?)
(or can saving the order in onReturn
be prevented? (is it necessary?))
Thanks for any help!
doesn't.
More detailed description of the problem: (This can be skipped)
The remote payment provider's server sends notification-requests requests to mythe server, about the status of the payment, so iI have both onReturn
and onNotify
in mythe PaymentGateway class.
Since onReturn
is not guaranteed to be called (the customer might close the browser, etc, and the provider does not necessarily send him back in my case), but onNotify
is guaranteed to be called, iI create and save mythe Payment
-object object in onNotify
, not in onReturn
, when the payment is complete.
(This is also what the documentation suggests to do: https://docs.drupalcommerce.org/commerce2/developer-guide/payments/create-payment-gateway/off-site-gateways/handling-ipn)
So my code looks something like this. (veryIt's a very simplified pseudo-code +code; validation checks notaren't included here.):
class RedirectCheckout extends OffsitePaymentGatewayBase implements SupportsNotificationsInterface {
...
public function onReturn(...) {
$is_order_accepted = ...check if the remote payment provider has accepted the order...
if (!$is_order_accepted)
throw new NeedsRedirectException(...)
// if all is good, do nothing
}
public function onNotify(...) {
/** @var OrderInterface $order */
$order = ...load the order the notification is about...
$is_order_accepted = ...check if the remote payment provider has accepted the order...
if ($is_order_accepted) {
$payment = $payment_storage->create(...);
$payment->save();
$order->setData('transaction_id', $transactionId);
$order->save(); // <- THIS IS WHAT SOMETIMES IS OVERWRITTEN BY onReturn(), I BELIEVE.
}
}
}
class RedirectCheckout extends OffsitePaymentGatewayBase implements SupportsNotificationsInterface {
public function onReturn() {
$is_order_accepted = /* Check the remote payment provider has accepted the order */
if (!$is_order_accepted) {
throw new NeedsRedirectException()
}
// If all is good, do nothing.
}
public function onNotify() {
/** @var OrderInterface $order */
$order = /* Load the order the notification is about */
$is_order_accepted = /* Check the remote payment provider has accepted the order */
if ($is_order_accepted) {
$payment = $payment_storage->create();
$payment->save();
$order->setData('transaction_id', $transactionId);
$order->save(); // This is what sometimes overwritten by onReturn(), I believe.
}
}
}
Notice that iI need to save some data on the order, when an order is accepted (which is not available when the order is created - onlybut when a payment succeeds).
(The Drupal Commerce documentation says you "don't need to (and should not)" touch the order, but iI have to save some extra data on the order, that other parts on the system expects to be there. Interesting what they mean by "(and should not)"...)
This often works. However, the two onReturn
and onNotify
requests from the remote server sometimes arrives at almost the same time, which iI believe leads to a race condition. (My current theory)
Unfortunately, even though i'mI am not doing anything to the order in onReturn
, commerceCommerce seems to still save the order. I belivebelieve this can sometimessometime overwrite the data saved to the order by onNotify
. ExampleFor example:
onReturn
starts running, and loads the order (This is done by the commerce library itself, so iI can't do anything about this - or can i?.)
- Now, theThe notify-request request arrives, so
onNotify
starts running, loads and saves the order, and returns.
- After this, the
onReturn
method returns, gives back control to commerceCommerce, which saves the order again - butagain; since it loaded the order-object it BEFORE object before onNotify
saved it, it overwrites whatever onNotify
wrote with old data, when it saves it.
(Perhaps the converse order can also be problematic, where onNotify
can overwrite any data saved to the order by Drupal Commerce behind the scenes (if, if any), during the onReturn
-request request.)
Is there a good way to handle this? (I.e., for example work around the race-condition to be able to save order-data in onNotify
)
How would you do it?
(I have thought about Drupal's locking mechanism (would have worked, if it was possible to lock in onReturn
all the way from before Drupal Commerce loads the order until after it has saved it - is that somehow possible?
Perhaps it's somehow possible to reload the orderdata in onReturn
(and wait for onNotify
to have completed)? Or create a wrapper (custom Controller) around onReturn
, which calls onReturns
, but locks first?). Perhaps there are other ways of ->setData
on the order which would survive, like "UPDATE commerce_order SET transaction_id = x WHERE y" instead of an object? Etc etc.)
I'm using Drupal 8.6, btw.