Order Changes
You edit a placed order by staging a change and confirming it, never by recalculating it in place. This page is for a store administrator editing orders, taking returns, or deciding whether to turn the feature on.
Before you begin
- Order changes need the
order_versioningfeature switched on. See Turn order versioning on. - Changing an order needs
kahunacart.orders.manage.
Staged, then confirmed
A change is a proposal. Opening one writes a row in kahunacart_order_changes. Adding actions to it writes rows in kahunacart_order_change_actions. The order's lines, adjustments, totals and statuses stay exactly as they were until somebody confirms.
A change that is never confirmed changes nothing. You can build one up over several sessions, look at the previewed total, and walk away.
Confirming appends and never removes. It increments the order's version, copies every current line and adjustment into the new version, applies the actions to the copies, and leaves the previous version's rows untouched. Version 1 reads back unchanged however many edits follow.
Important
There is no Recalculate button that rewrites a placed order in place. Recalculating a paid order as though it were a cart is how line items get silently deleted from orders somebody has already paid for.
The action vocabulary
A change is built from typed verbs. Which verbs are legal depends on the kind of change.
| Verb | What it does |
|---|---|
add_item |
A line the order did not have |
write_off_item |
Take a line out of the current version. The original line stays readable at its own version. |
reinstate_item |
Put a written-off line back, at the price it was bought under |
update_quantity |
Change how many of a line there are |
update_price |
Change what a line's units cost |
add_adjustment |
A discount, a fee, a manual credit |
remove_adjustment |
Take an adjustment out of the current version |
update_shipping |
Change what postage costs |
update_address |
Correct a billing or shipping address |
update_tax |
Correct the tax |
add_payment |
Record money taken outside KahunaCart for the difference |
refund_payment |
Money going back, through the existing refund path |
return_item |
Units of a line coming back |
There is no delete_item. Removing a line is write_off_item, which means "do not carry this line into the new version". The line itself is never written to and never deleted.
| Kind of change | Verbs it may use |
|---|---|
edit |
Everything except return_item |
exchange |
Everything |
claim |
Everything |
return_request |
return_item, add_adjustment, refund_payment |
return_receive |
return_item, write_off_item, add_adjustment, refund_payment |
Edit a placed order
- Open the order in KahunaCart → Orders.
- Click Stage a change, pick Edit, and give a reason.
- Add actions. Each one updates the previewed totals beside the change. Nothing is written to the order yet.
- Read the preview. It runs the store's real pricing pipeline over the projected order, so the total shown is the total a confirm produces.
- Click Confirm change.
The order moves to the next version, its lines and adjustments are the new version's copies, and an order.changed entry appears on the order's history naming you.
To abandon the change instead, click Discard change. The change and its actions stay on the record so a withdrawn proposal can still be read.
In another currency
Every amount in a change is in the order's own currency, and the preview and the confirmed version are too. A yen order's change reads in whole yen and a euro order's in euro, and the admin reads a typed amount with the order's currency's decimal places: 1500 is 1500 yen, 12.50 is 12.50 euro, and an amount with more decimals than the currency has is refused before anything is sent.
On a store that sells in more than one currency an add_item that names a variant_id is priced for you, in the order's currency, from the product's amount there. The admin offers the catalogue in a list that shows each product's price in the order's currency, and a product with no price in it is greyed out with "Not sold in EUR". A product with no price in the order's currency cannot be added, and the server refuses it with 422 even when a price is typed, because a typed price would still be selling something nobody priced for that currency. A price you type for a product that does have one wins over the catalogue's, and a custom line (no variant_id) is priced by the amount you send. The snapshot kept on the line says which currency its price was in.
A store with one currency prices nothing for you, as before. The price is whatever the request carries.
Read an order at an earlier version
- Open the order.
- Choose a version from the version selector beside the order number.
The lines and adjustments shown are the ones the order carried at that version, including lines a later change wrote off. Historical versions are read-only.
GET /kahunacart/orders/{id}/versions/{v} answers the same thing.
Unlock a closed order
An order whose status is marked is_final (canceled, archived, or anything you flagged that way) needs unlocking before a change can be staged against it.
- Open the order.
- Click Unlock for editing and give a reason.
The unlock is never refused. It writes an order.unlocked entry on the order's history naming whoever pressed it. Adding a forgotten line to a closed order never means raising a second order; it costs one audited action that somebody owns.
Returns
Returns are core. A return is a change of type return_request or return_receive.
return_request records what the customer says is coming back. It moves no stock and no money.
return_receive records what actually turned up. Each return_item action carries two quantities.
| Field | Meaning |
|---|---|
received_qty |
How many units turned up |
damaged_qty |
How many of those are damaged |
Damaged units do not go back into stock. Receiving 2 of 3 with 1 damaged puts exactly 1 unit back on the shelf. The restock goes through the same atomic stock path every refund restock uses, and it only touches variants that manage stock.
Refunding the return uses the existing refund path unchanged: the same allocator, the same per-line quantities, the same provider call. See Refund an order.
Note
The customer-facing side — the return form on the order page, approval, return numbers and labels — is Returns. It records a return_request change when you approve a return and a return_receive change when you check its parcel in. Those two are recorded in place: a change made only of return_item actions edits no line, so it writes no new version of the order, every line keeps its id, and it works whether or not order versioning is on and without unlocking a closed order. OrderChangeService::recordReturn() is that one-step door, for an add-on that receives goods some other way.
A return holds the order's lines. While a return on the order is open, approved or received, confirming any other change is refused with a message naming the return ("Return R1042-1 is still active on this order…"), because a confirmed change writes a new version and gives every line a new id, and the return and the refund that follows it name the ids the customer sent back. The change stays staged: confirm it once the return is refunded, declined or cancelled. The return's own in-place records are not affected.
Turn order versioning on
Order changes are off until you turn them on. KahunaCart refuses to turn them on while an installed add-on has not declared that it understands versioned orders.
- Go to KahunaCart → Settings → Features.
- Read the blocking list beside Order versioning. It names any installed KahunaCart add-on that has not declared support.
- Update or switch off the plugins it names.
- Click Turn on.
A store with no add-ons has nothing blocking it and turns it on in one click. Switching a feature off is never refused.
Warning
An add-on that reads kahunacart_order_items or kahunacart_adjustments directly must filter on superseded_by_version IS NULL or it reads an edited order's lines twice. Everything that goes through OrderRepository already does. This is what declaring compatibility with order_versioning asserts.
An add-on declares support on onKahunaCartDeclareFeatureCompatibility:
public function onKahunaCartDeclareFeatureCompatibility(Event $event): void
{
$event['features']->declare('kahunacart-licenses', 'Licenses', [
'order_versioning',
'order_status_registry',
], '1.2.0');
}
An add-on that declares nothing blocks every feature. An add-on written before a feature existed cannot have declared for it, and silence is not treated as consent.
Configuration
| Config key | Default | What it does |
|---|---|---|
features.order_versioning |
false |
Whether a fresh store starts with staged order changes available. Your choice on the Features screen is stored in the database and wins over this. |
Events
| Event | Payload | When |
|---|---|---|
onKahunaCartOrderChangeStaged |
order_id, change_id, change, action |
A change was opened, or an action was added to one. Nothing has been written to the order. |
onKahunaCartOrderChangeConfirmed |
order_id, change_id, change, version_from, version_to, order, restocked |
A change was applied to a new version |
onKahunaCartDeclareFeatureCompatibility |
features, codes |
Declare which core features your add-on understands |
API and MCP tools
| Route | Tool | Permission |
|---|---|---|
GET /kahunacart/orders/{id}/changes |
list_order_changes |
kahunacart.orders.view |
POST /kahunacart/orders/{id}/changes |
create_order_change |
kahunacart.orders.manage |
POST /kahunacart/orders/{id}/changes/{cid}/actions |
add_order_change_action |
kahunacart.orders.manage |
POST /kahunacart/orders/{id}/changes/{cid}/confirm |
confirm_order_change |
kahunacart.orders.manage |
POST /kahunacart/orders/{id}/changes/{cid}/cancel |
cancel_order_change |
kahunacart.orders.manage |
GET /kahunacart/orders/{id}/versions/{v} |
get_order_version |
kahunacart.orders.view |
POST /kahunacart/orders/{id}/unlock |
unlock_order |
kahunacart.orders.manage |
GET /kahunacart/features |
list_features |
kahunacart.orders.view |
POST /kahunacart/features/{code}/enable |
set_feature |
kahunacart.orders.manage |
Confirming is idempotent. A repeated confirm_order_change answers with applied: false, the version is not bumped twice, and no unit is restocked twice.
Confirmation locks and reads the order again on each transaction attempt. If another confirmation changes the order while a failed attempt is retried, your change stays staged. Review it and try again.
Related
- Order statuses — the flags, and the
is_finalthat decides whether an order needs unlocking - Orders and the admin — refunds, mark-paid, shipments and the rest of the order detail
- Products and catalog — stock, and what a restock touches
- Configuration —
features.order_versioning - Events — the three events on this page
- REST API — every route with its permission
- Currencies — selling in more than one currency