In the course of working on our specific business case, as well as integrating with the latest changes in Tax, it appears we need the ability to allow order refresh to occur on non-draft orders, as well. Currently Order module will abort automatic refresh in the default configuration if the order is not a draft. However, it is possible to manually set the refresh state or call the refresh manager directly.

The reasons for doing this vary, but all have to do with responding to changes to the order after it is placed but before it's considered "complete." In our case, this means adjustments during fulfillment (e.g., changes to the actual quantity we can ship) but mglaman points out this relates to backorders and other types of order validation workflow. The goal of refreshing the order is essentially to call order processors, which could for instance re-calculate tax.

The logic in OrderRefresh::refresh() performs other tasks, however, that are destructive to non-drafts. Order item unit prices are re-set (which would appear to be generally undesirable) and the order item's order ID relationship is removed, which is obviously not good. There is a @todo note to "evaluate which order items have changed," which would be more or less what we need to do.

In the short term I have manually triggered tax re-calculation and other processor calls in pre-save hooks, but that isn't a sustainable solution for Commerce Order. What do the maintainers think about this, particularly since it touches some common use-cases like backorder?

One example of an edge case we will need to address: The availability order processor does not pass through the order status to the various availability plugins, only a global Context object. This is rational because there may not actually be an order, yet, but if a product resolves as available only because it's not yet in a placed order, it will be removed during the order refresh because we lack context to decide otherwise.

Issue fork commerce-2875804

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

bradjones1 created an issue. See original summary.

agoradesign’s picture

Touching non-draft orders sounds dangerous to me. So this should definitely not be done by default. Maybe introduce a config setting?

mglaman’s picture

Touching non-draft orders sounds dangerous to me.

Agreed. I don't know the solution. But we do need a way for people using Fulfilment / Validation workflows to reconcile the order if quantities are adjusted.

I don't know if this involves adding sort some kind of "re-open" transition? I'd like to see how other systems handle this issue first. Research is key here.

bojanz’s picture

Order refresh will definitely stay limited to draft orders, cause as Brad pointed out, it does a number of things that are not safe to do to a placed order.
We need a different/parallel process for recalculating placed orders. Question is whether that process should still invoke order processors or not.

bradjones1’s picture

My vote would be to re-use as much of the existing Commerce API as possible, including order processors. I am essentially doing this through some custom hooks (as described in the summary) and so far, most of the default order processing is compatible with this workflow. The only exception so far appears to be product availability, but only if you are implementing logic that limits the availability of products after order placement. I'm not sure where commerce_stock is on this since I'm not using it, but I just un-tagged my availability plugin and am running it directly inside a new order processor, so as to have access to the $order's state.

Re: mglaman's thought about a "re-open" state, it seems like that might be more than we need out of the box? It seems to me that the use case presented here kind of assumes the order isn't complete, yet, and we have a "complete" state out of the box already. This pertains to the middle-ground between placement and completion, where the order is POST-processed. There's not much if anything in Commerce 2.x yet about protecting completed orders, anyway, so data integrity in those various steps is just another feature we can add on as the project grows. But at the moment there's still a lot of "don't touch" you must enforce manually as part of your business process, so tackling that huge question just for the sake of order adjustments post-placement seems like a lot to bite off.

Thanks everyone for considering this.

andrewbelcher’s picture

Another side of this is orders that 'stay' open. In my use case, we have bookings for an event that are potentially created 12 months ahead of time. Over the 12 month period, there are a few pricing windows for tickets and customers can come in any time during that period and add additional tickets or make additional payments. Once a ticket is paid in full, the price is locked.

The refresh process seems ideal for our needs, as refreshing when the order is loaded is ideal for ensuring the information is always up to date, and we can trigger a refresh of all items when a pricing window passes in the background. However, this would suggest that we have to leave our orders in the draft state.

Perhaps an ability to configure what states should be recalculated would be a good way to make this more flexible, draft by default, but order types can specify both the interval and what states should be recalculated. I suppose an alternative would be to be able to have a 'draft' flag on a state, but I wonder if that's more complex for no real benefit...

bojanz’s picture

Marked #3095678: Allow placed orders to be refreshed as a duplicate.

Quoting it:

For a long time we've had the need to refresh placed orders.
Example use case:
An order is placed with a tax number that could not be verified ("verification_state" => "unknown").
The merchant reverifies the tax number, and it fails validation.
The order should not be recalculated so that tax is re-applied, since the tax-free status is no longer valid.

We've just done some needed work in this area:
#3093784: Add the concept of an order "calculation date"
#2883789: Create a $store->timezone field and use it for promotions and taxes
This ensures that the $order->placed date is used in the order refresh when available, ensuring that the right promotions and taxes are still selected.

The next step would be to expand OrderRefresh::refresh() to allow refreshing non-draft orders.
In case the order is not a draft, we would not recalculate the prices, just run the processors to recalculate the adjustments (promotions/taxes).

The harder part is figuring out how the merchant should trigger this operation. Guessing we should err on the side of not making it automatic in any way. So, perhaps a button on order view?

Lisa also had a good point that this process is needed for placed-but-not-yet-complete orders:

One design feature that would be helpful is making its applicability configurable by order state. So, for example, manual refresh is allowed for placed/validation/processing order flow steps. But then at some point, the order is "finalized", and order refresh should not be available. This restriction would be particularly helpful with respect to relationships to accounting systems. Whenever an order has been invoiced, i.e., posted as a sale in an accounting system, that total order amount should remain unaltered going forward.

dercheffe made their first commit to this issue’s fork.

dercheffe’s picture

Sorry didn't want make a commit to this issue fork just a normal comment :-/ Just delete my commit please.

I have another point that might have a relation to this issue:

Use case:

  1. An admin (or sales team too) get a call from a customer. They fill out the order add form (select order type and the shop) and the order is created.
  2. in the next step the admin/sales person gets redirected to the edit form of the new created order to select order items override prices etc. In this form the admin/sales person is able to set the order state as completed (accidents happen)
  3. If the order state is set as completed the order is saved and all the order processors aren't firing (e.g. taxes)

perhaps a simple bool option or a bool field makes sense if the order processor's may fire or not?

Robert S’s picture

OK, I understand this is a tricky issue with many edge cases.

However, as it stands, it appears the commerce_tax module does not work well with editing the order -- that is with editing anything that will affect the order total, and thus the tax. In my case, we often need to adjust quantity based on availability (selling fabric by the meter, it is not easy to measure precisely in advance). I suppose many people have this issue, after all, that is the purpose of "Fulfilment / Validation workflow".

So, is there some recommended workaround? Adding a button to manually do order refresh?

kaszarobert’s picture

We also encountered this issue numerous times. Order came in, the customer later called the shop to add something else to the order or change the shipping method, etc. The result was always a frustrating talk about why the prices and taxes are now wrong on the order and on the sent invoice. For non technical people this is very confusing. The worse is that on the order's form you don't even have a chance to at least fix the wrong or missing adjustments at the order item level (such as promotion discounts and taxes). The order item's adjustments field is not editable on the UI.

Other webshops solve this by showing a recalculate button on the order edit forms. So I also support that manual order refresh-alike idea.

If that's not a viable option, implement some guard or validation that blocks editing everything that changes the price for non draft orders. So it would be impossible to break things with invalid adjustment values. That way at least everyone would understand that if I want to edit an order, I must cancel it and make a new one instead.

jsacksick’s picture

Wondering if we should focus on fixing the admin order edit first? Perhaps add a separate submit button that allows explicitly refresh the order? (if the order is not a draft).

If the order is a draft, it'll be refreshed on presave anyway. If it's not a draft, we could allow triggering the refresh via a separate button, though we probably should skip refreshing prices, or better, allow opting out price refresh?

There are still several problems around refreshing a placed order:

  1. Prices may have changed since the order was placed (the solution here could be to allow opting out refreshing prices)
  2. Promotions / coupons may not be applicable anymore (For that particular problem, I have a working solution on several of my projects). The solution consists in collecting the promotion IDS from the existing adjustments before clearing them, then reapplying the promotions.
rhovland’s picture

I've been working at this issue for our store for a couple months now and after debating a lot of ways of approaching this I think I've figured out what is a sane way of addressing this.
After an order has been placed any edits to an order are in the admin domain. A trained person is doing operations on the order. Each store is going to have their own process.
The very first thing commerce needs for administrators is controls, not automatic processes. If a store wants something to automatically happen they could call the function the manual control triggers in an event hook or whatever is appropriate. But for everyone else they would have their own processes to follow which could include clicking a button to run a certain type of order processor.

Promotions:
Some stores may wish to honor promotions even after the order has changed and no longer qualifies for a promotion. Other stores want to recalculate promotions when the order is edited. We need both a button to recalculate all order promotions, and a way to do it on a per item basis. We need to be able to choose if it recalculates using the promotions that were active at the time the order was placed, or the current promotions.

Taxes:
Tax calculation is often whole order dependent (taxing authority rules) so providing a form with a button to recalculate taxes on the order makes sense. Other tax modules could extend that form to take other actions such as submitting an adjustment to the transaction to a tax recording service.
The tax recalculation form should also document that taxes were calculated and why. This would be written to the order log and the reason could also be used by tax modules for record keeping purposes.

There's a few other parts of the order refresh I haven't touched on yet but this is what I've considered so far.

xmacinfo’s picture

Yes, we need some way to refresh the calculation instead of deleting an order and recreating it. It could be a button available only to a user with a specific role.

In my use case, I created an order and forgot to add the billing information. I placed the order too quickly before checking if everything was fine.

I edited the order to add the correct billing information, but the taxes won't refresh.

rhovland’s picture

@xmacinfo See the attached child issue. I added a form to refresh taxes
https://www.drupal.org/project/commerce/issues/3333475

m_z’s picture

I came across this issue and want to report how you can manually trigger an order refresh if you have an order type with shipping.

On the /admin/commerce/orders/ORDER_ID/shipments tab you can "edit" an existing shipment for your order.
You can active the checkboxes for all shipment items and then click the "Recalculate shipping" button.
Don't forget to click the "Save" button at the bottom.

Afterwards you should see the correct tax values in the order summary on /admin/commerce/orders/ORDER_ID tab.

This workaround-like solution should work for a shop administrator with proper permissions.

------

(For developers you can use devel_php or a custom module to execute the following code for the $order entity that should be refreshed:

$orderRefresh = \Drupal::service('commerce_order.order_refresh');
$orderRefresh->refresh($order);

But this solution isn't suitable for a typical shop administrator, so I want to share my shipping refresh solution above.)

------

EDIT / UPDATE:

My explanation above isn't really right. The order refresh will only be triggered if the new line items and the new order total lead to another shipping option. Then the whole order will be refreshed (like stated above).

Moreover I found out that a complete order refresh does a lot of things (e.g. see https://www.susubi.io/sa/editing-placed-orders) and especially the price and / or quantity changes are highly unwanted in my use case. I only want to make it possible for the shop administrator to re-calculate the taxes for the order total.

For my use case I implemented a custom module based on the solution and the patch of comment 6 in https://www.drupal.org/project/commerce/issues/3333475#comment-14980510 and this works very well. It only re-calculates the taxes without doing the other "order refresh" stuff...

introfini’s picture

Related contrib module: Commerce Order Amend

I've published a contrib module that addresses the practical need for editing placed orders while working around the limitations described in this issue:

Commerce Order Amend (1.0.0-beta1)

It provides an "Amend Order" tab on placed orders with operations for swapping item variations, adding/removing items, and managing coupons.

To deal with the destructive nature of OrderRefresh on placed orders, the module:

  1. Locks all item prices before refresh by setting overridden_unit_price on every order item, preserving the original checkout pricing
  2. Compares order state before and after refresh to detect side effects (promotions added/removed, tax recalculations)
  3. Warns staff about any unexpected changes with a link to the order edit form

It uses $order->setRefreshState(OrderInterface::REFRESH_ON_SAVE) to force recalculation of promotions and taxes, but the price locking prevents the unit price re-resolution that makes blanket refresh unsafe for placed orders.

Key implementation details

Before calling refreshAndSave(), the module locks every order item:

  // Lock prices on existing items to prevent re-resolution.
  foreach ($order->getItems() as $item) {
    if (!$item->isUnitPriceOverridden()) {
      $item->setUnitPrice($item->getUnitPrice(), TRUE);
      $item->save();
    }
  }

  // Capture snapshot for diff.
  $snapshot = $this->captureOrderSnapshot($order);

  // Force refresh.
  $order->setRefreshState(OrderInterface::REFRESH_ON_SAVE);
  $order->save();

  // Compare and report side effects.
  return $this->diffOrderSnapshot($snapshot, $order);
  

The diff compares adjustments by type (not label) to avoid false positives from translated labels (e.g., "Envio" vs "Shipping" both map to the shipping type).

This is a workaround rather than a core solution. If a selective refresh API lands in core (as discussed in this issue), the module will adopt it for more granular control over which processors run during amendments.