Hello,
With 3D secure verifications, offsite payments such as Paypal, or long offsite checkout flows like Stripe Checkout, customers can be away from the site for some time with their order locked. By the time they come back, a promotion can have expired or a new one can be active.
As of right now, you can witness that expiring a promotion while waiting for the customer to confirm/finish an offsite payment will result in the order's total amount being modified and not corresponding to the amount that really has been paid anymore. This even though the order was locked.
We should make sure that the price of a locked order never changes.
To ensure this, I believe the following functions should be updated to do nothing if the order is locked or not 'draft' anymore:
Order::recalculateTotalPrice
OrderRefresh::refresh
PromotionOrderProcessor::process
There might be other functions that need reinforcement, these are the only ones I found needed to be secured so far.
If you face this problem too and would like to fix this while this is not officially included yet, here is how I've implemented this on our site with no need for a patch:
I've created 3 custom classes:
class MyModuleOrder extends Order {
public function recalculateTotalPrice() {
if ($this->isLocked() || $this->getState()->value !== 'draft') {
return $this;
}
return parent::recalculateTotalPrice();
}
}
class MyModuleOrderRefresh extends OrderRefresh {
public function refresh(OrderInterface $order) {
if ($order->isLocked() || $order->getState()->value !== 'draft') {
return;
}
parent::refresh($order);
}
}
class MyModulePromotionOrderProcessor extends PromotionOrderProcessor {
public function process(OrderInterface $order) {
if ($order->isLocked() || $order->getState()->value !== 'draft') {
return;
}
parent::process($order);
}
}
In order to override Commerce's default classes with mine, this is what I did:
For Order it's pretty simple:
function my_module_entity_type_alter(array &$entity_types) {
if (isset($entity_types['commerce_order'])) {
$entity_types['commerce_order']->setClass(MyModuleOrder::class);
}
}
For OrderRefresh and PromotionOrderProcessor, because they are Services, we need to use a custom ServiceProvider that extends ServiceProviderBase as decribred here:
https://www.drupal.org/docs/drupal-apis/services-and-dependency-injectio... →
Be aware to name it exactly in PascalCase MyModuleNameServiceProvider and place it in my_module/src/ so that it is detected:
class MyModuleNameServiceProvider extends ServiceProviderBase {
public function alter(ContainerBuilder $container) {
$order_refresh = $container->getDefinition('commerce_order.order_refresh');
$order_refresh->setClass(MyModuleOrderRefresh::class);
$promotion_order_processor = $container->getDefinition('commerce_promotion.promotion_order_processor');
$promotion_order_processor->setClass(MyModulePromotionOrderProcessor::class);
}
}