I'm new to Drupal and have to make a Offsite Payment Gateway. (With 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 that 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!
.
More detailed description of the problem: (This can be skipped)
The remote payment provider's server sends notification-requests to my server, about the status of the payment, so i have both onReturn
and onNotify
in my 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, i create and save my Payment
-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 (very simplified pseudo-code + validation checks not 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.
}
}
}
Notice that i need to save some data on the order, when an order is accepted (which is not available when the order is created - only when a payment succeeds).
(The Drupal Commerce documentation says you "don't need to (and should not)" touch the order, but i 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 i believe leads to a race condition. (My current theory)
Unfortunately, even though i'm not doing anything to the order in onReturn
, commerce seems to still save the order. I belive this can sometimes overwrite the data saved to the order by onNotify
. Example:
onReturn
starts running, and loads the order (This is done by the commerce library itself, so i can't do anything about this - or can i?)- Now, the notify-request arrives, so
onNotify
starts running, loads and saves the order, and returns. - After this, the
onReturn
method returns, gives back control to commerce, which saves the order again - but since it loaded the order-object it BEFOREonNotify
saved it, it overwrites whateveronNotify
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 any) during the onReturn
-request)
Is there a good way to handle this? (I.e. 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 order 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.