Skip to main content

Orders

An order is written by one class, changes status through one class, and is handed to plugins as one read-only object. This page describes those three, the helpers around them, and the two locks.

The events an order announces are on Order and cart events. Payments and parcels are on Payments and shipments.

What it does​

ClassIn plain wordsFor extensions?
OrderCreator (administrator/src/Service/Order/OrderCreator.php)Writes a new order and its lines from plain data, without a cartYes, with limits. The code's comments expect a plugin as a caller. No extension that ships with Alfa calls it. See Who may call it.
Transition (administrator/src/Service/Order/Transition.php)The single way an order changes statusYes for a payment plugin, through orderTransition(). For a feature plugin the class comment expects the call and nothing shipped shows it. For a shipping plugin the code does not settle it. See Transition.
Order (site/src/Service/Order/Order.php)The order as a plugin reads itRead only. Events hand it to you.
OrderStatusHelper (administrator/src/Helper/OrderStatusHelper.php)Answers "which status is the first one, which are cancelled, which are completed"Read only, called directly
OrderTotalHelper (administrator/src/Helper/OrderTotalHelper.php)The one formula of an order's totalCore's own. Described so that you can read a total correctly.
OrderHelper (administrator/src/Helper/OrderHelper.php)Reads an order's lines for the order screenCore's own
OrderHistoryHelper (administrator/src/Helper/OrderHistoryHelper.php)Turns a stored History entry into the line staff readCore's own
StockPolicy (administrator/src/Service/Order/StockPolicy.php)What an order status asks of the stockCore's own. Described so that you can follow the stock events.
OrderMoneyLock, ShipmentBookingLock (administrator/src/Service/Order/)One capture at a time per order, one booking at a time per shipmentTheir class comments address a payment plugin and a shipping plugin. Nothing in 2.2.0 calls them, so there is no shipped example. See The two locks.

When it is used​

  • Your extension creates orders that do not come from a cart, for example a renewal.
  • Your extension must move an order to another status.
  • Your plugin is handed an order in an event and you want to know what you can read.
  • You show or compare an order's total and want the same figure as Alfa.

OrderCreator​

What it does​

OrderCreator::create() writes one order row, its lines, sets the stock aside, and writes the "order created" entry of the order's History. It reads no cart, no request and no catalogue price: every figure is in the data you pass.

The checkout itself uses it: OrderPlacement builds the data from the cart and calls create(). The back office uses it for a manual order.

It is deliberately narrow. It sends no e-mail, announces no event, and writes no payment row and no parcel. onAlfaOrderPlaced is announced by the checkout, not by OrderCreator.

When it is used​

When an order has to exist and there is no shopper at a checkout. The class comment names renewals and manual orders next to the checkout.

Who may call it​

What the code saysThe class comment calls it the writer "for every non-checkout order source" and names renewals. A comment inside create() speaks of a plugin that supplies the source, and the comment of clampTaxName() calls that method "a helper for a plugin". So the code expects a plugin as a caller.
What the code does not give youNo plugin or module that ships with Alfa Commerce calls create(): the two callers in 2.2.0 are the checkout and the back office. There is no shipped example and no base class method that reaches it.
No permission is checkedcreate() checks no user right. Alfa's own callers check the person before they call. Your code must do the same.
It is not a complete orderIt writes no payment row and no parcel. How an extension that is not the order's payment or shipping plugin adds them is not settled by the code. See Not documented yet.

What you must provide​

public static function create(array $draft, bool $manageTransaction = true): object

The draft is a plain array. Keys marked required must be there; the others have the default shown.

KeyMeaningDefault
sourceRequired. Where the order comes from: storefront, admin, renewal or api. It is stored on the order and in its History. Only its presence is checked.
linesRequired, at least one line, unless allow_empty is true
id_address_deliveryThe id of an existing address row (#__alfa_user_info). The method's comment marks it required, but create() does not check it: without it the order is stored with no delivery address. Planned for 2.2.1, which is not released: required when the draft names a shipping method (id_shipment_method), and checked; an order with no shipping method needs none.none
id_address_invoiceThe same for the invoice addressnone
id_userThe customer's user id0, a guest
id_order_statusThe status the order starts inThe shop's first status
id_currencyThe currencyThe shop's default currency
conversion_rate1.0
id_payment_method, id_shipment_methodThe methods0, none
payment_method_name, shipment_method_nameThe methods' names as kept on the orderempty
id_languageThe language the customer is addressed in1
customer_note, noteNotesempty
ip_addressThe current request's address
created_byWho created itThe current user
deduct_stockWhether the order claims stocktrue
allow_emptyAllows an order without linesfalse

Each line:

KeyMeaningDefault
id_itemRequired. The item
quantityRequired, above 0
unit_price_tax_excl, unit_price_tax_inclRequired. The unit price without and with tax
id_combinationThe combinationnone
nameThe name kept on the lineTaken from the catalogue
sku, gtin, mpn, weightempty, 0
total_price_tax_excl, total_price_tax_inclThe line totalsUnit price times quantity
original_product_priceThe price before discountsunit_price_tax_excl
tax_rate, tax_nameThe line's tax0, empty
discount_percent, discount_amount_tax_excl, discount_amount_tax_inclThe line's discount0
id_shipment_methodA method for this lineThe order's

A minimal call. Every key was checked against create() in 2.2.0:

use Alfa\Component\Alfa\Administrator\Service\Order\OrderCreator;

$created = OrderCreator::create(draft: [
'source' => 'api',
'id_user' => $customerId,
'id_address_delivery' => $addressId, // an existing #__alfa_user_info row
'lines' => [
[
'id_item' => $itemId,
'quantity' => 2,
'unit_price_tax_excl' => 10.00,
'unit_price_tax_incl' => 12.40,
'tax_rate' => 24.0,
],
],
]);

$orderId = $created->orderId;
$statusId = $created->statusId; // where the order landed, see below

$manageTransaction: leave it true and create() saves as one unit of its own. Pass false only when your code has already opened a database transaction and will make the save final itself.

What happens next​

create() returns an object with orderId, lineIds (in the order of your lines) and statusId.

What it guarantees:

All or nothingThe order, its lines, the stock claim and the History entry are saved together. If one fails, none is kept.
The stock follows the statusThe order claims stock as its starting status asks: nothing for a cancelled status, a reservation, or the goods themselves. See StockPolicy. A claim that fails makes the whole creation fail.
A status that does not exist does not lose the orderIf id_order_status names a status that does not exist or is unpublished, the order lands in the shop's first status and staff get a notice. Read statusId to see where it landed.
Prices are kept as givencreate() never prices a line again.

After it returns, the rest is yours: a payment row and a parcel are written by the payment and shipping plugins of the order's methods, a status change goes through Transition.

What can go wrong​

create() refuses by throwing a RuntimeException. When it owns the transaction, everything is undone first.

It refusesMessage starts with
A draft without sourceOrderCreator: a draft must name its source
lines that is not an arrayOrderCreator: lines must be an array
No lines, without allow_emptyOrderCreator: a draft needs at least one line
A line without id_item or with a quantity of 0 or lessOrderCreator: line N needs id_item and a positive quantity
A line without both unit pricesOrderCreator: line N needs explicit unit prices
A starting status that requires the order to be paid, unless that status allows an unpaid exceptionOrderCreator: status N requires payment
A shop with no order status at allOrderCreator: no target status and no is_initial status exists
Planned for 2.2.1, which is not released: a draft that names a shipping method and no existing delivery address. Nothing is written.OrderCreator: id_address_delivery must name an existing delivery record
Any write that fails, the stock claim includedOrderCreator: order creation failed:

Two more public methods:

MethodWhat it does
mayCreateInto(int $statusId): boolWhether a new order may start in this status. false for a status that requires payment and allows no unpaid exception. Ask it before you offer a status as a starting status.
clampTaxName(string $name, string $context): stringShortens a tax name to 255 characters (OrderCreator::TAX_NAME_MAX) and logs when it had to. For a plugin that keeps a tax name in a column of its own of that width. Core does not call it in 2.2.0: an order line keeps the tax name whole.

Transition​

What it does​

Transition is the single way an order changes status. Staff, a payment, a parcel and a plugin all end here, so the follow-up work is the same whoever asked: the stock moves, download access is granted or removed, documents are issued, the status e-mail is sent and onOrderStatusChange is announced.

The picture of a status change is on Event flows. The listener's side is on Order and cart events.

When it is used​

You areUse
A payment or shipping plugin, and the move follows a payment or parcel statusNothing. The method's settings status_on_<status> move the order. See The order's status and A parcel's status can move the order.
A payment plugin, for a moment the settings cannot express$this->orderTransition($orderId)->to($statusId)->save()
A feature pluginTransition::for($orderId)->to($statusId)->byPlugin($element)->save(). The class comment counts a plugin that is not a payment plugin among its callers. No plugin that ships with Alfa makes this call, and no base class offers it, so there is no shipped example.
A shipping plugin, for a moment the settings cannot expressThe code does not settle it. ShipmentsPlugin has no orderTransition(). This manual documents only the settings for a shipping plugin.

Transition checks no user right. Alfa's own callers check the person before they call. A plugin that moves an order because a person asked must check that person's rights itself.

Never write the column id_order_status yourself. The status would change and none of the follow-up work would happen.

What you must provide​

use Alfa\Component\Alfa\Administrator\Service\Order\Transition;

$transition = Transition::for($orderId)
->to($statusId)
->byPlugin('thanks') // your plugin's element
->note('Loyalty tier reached');

$moved = $transition->save();

if (!$moved && $transition->refusedBusy()) {
// Another request held the order. Asking again later may move it.
}

The calls you chain, each returning the same object:

CallMeaning
Transition::for(int $orderId): selfStarts a change for an order
to(int $statusId)The status to move to. Required.
byPlugin(string $pluginName)Records your plugin as the one that moved the order. Pass the plugin's element.
byUser(int $userId)Records a user instead. Without either, the current user is recorded.
note(string $note)A sentence for the order's History
silent()Sends no status e-mail
expectStatus(int $statusId)Since 2.2.0. Moves the order only if it is still in this status. Use it when your move was decided earlier and the order may have moved since.
force()Allows the order to leave a cancelled status. That is a decision for a member of staff. Automatic code must never call it.
from(int $statusId)For Alfa's own order screen, which writes the status itself first. Do not call it.
save(): boolMakes the change. true when the order moved.

What happens next​

save() does this, in this order, with the order's row held so that no other request can change it meanwhile:

  1. Reads the order's status as it is now.
  2. Checks that the target status exists, is published, and that the order may enter it.
  3. Writes the new status.
  4. Records, in the same save, the History entry and the follow-up work the change owes.
  5. Makes the save final and releases the order.
  6. Carries out the follow-up work.

What it guarantees:

The same move twice moves onceA second request for the status the order already has returns false and does nothing: no second e-mail, no second document. This is what makes a provider callback that arrives twice harmless.
The change and what it owes are saved togetherIf the change is undone, no follow-up work is left behind. If the request stops after the save, the work is still recorded and Alfa carries it out later.
Work only where there is workSince 2.2.0 a step is recorded only when it has something to do: the stock step when the new status asks something else of the stock, the documents step when the status issues documents, the e-mail when the status notifies, onOrderStatusChange when a plugin listens.
The follow-up work runs after the save is finalAnd after Alfa's locks are released. When you call save() inside a database transaction of your own, the work waits until your transaction is final. Until then the order has its new status and nothing else has happened.
A refusal that matters is visibleA change refused because of the target status, because the order is cancelled, or because the order was busy is written to the log (category com_alfa.orders). The first and the last are also written to the order's History.

The follow-up work, in the order it runs: stock, download access, documents, the status e-mail, onOrderStatusChange. Each step is a save of its own. A step that fails leaves the status as saved.

What can go wrong​

save() returns false and moves nothing when:

The reasonHow to tell
The order does not exist, or no target was given
The order is already in that status
The target status does not exist or is unpublishedTransition::isValidTarget($statusId) is false
The status requires the order to be paid, the order is not, and the status allows no unpaid exceptionTransition::refusalReason($orderId, $statusId) gives the reason
The order names no currencyThe same
The target status keeps the goods set aside, and a line of the order has a unit that does not track stock nowThe same
The order is in a cancelled status, the target is not, and force() was not called
The order was no longer in the status named with expectStatus()refusedSuperseded() is true
Another request held the order's row for too long, or the database stopped the change twicerefusedBusy() is true. Asking again later may work.

outcome(bool $moved): string turns the answer of save() into one of four constants: Transition::OUTCOME_MOVED, OUTCOME_UNMOVED, OUTCOME_BUSY and OUTCOME_SUPERSEDED (since 2.2.0).

Any other error is thrown. When save() runs inside your transaction and the database stops it to resolve a deadlock, that error is thrown to you as well, because your own work is lost with it.

Reading what the stock did, after save() returned true:

MethodMeaning
getStockResult(): ?array['action' => ..., 'count' => ...] with the action deducted, reserved, restored or none. null when the stock step has not been applied: it is still owed, or it was refused. While a transaction of your own is open and a stock step is owed, it is null too.
getStockRefusal(): ?stringWhy the stock step was refused for good, or null
getStockReservedFloor(): ?stringWhy the stock step waits for stock that other orders hold, or null
getStockFailure(): ?stringWhat the stock step failed with, when a later attempt can still apply it, or null

The three texts are plain text. Escape them before you show them in a page.

The two static methods drainFor() and deliverStockOutcome() are for Alfa's own background work. Do not call them.

The order reader​

What it does​

Order is what every order event hands a plugin. It answers with named methods and has no method that changes anything. Everything it gives is what the order recorded, not what the customer's account says today.

You get it from getOrder() of an event, for example onAlfaOrderPlaced, onOrderStatusChange, onOrderAfterPlace and the staff action events of a payment or shipping plugin. A payment or shipping plugin can also load one with $this->getOrder($orderId).

What you are given​

MethodReturnsMeaning
id()intThe order's id
reference()stringThe reference the merchant and the customer see
secureKey()stringThe key a return link is signed with
customerId()intThe account that placed it. 0 for a guest and for an account deleted since.
isGuest()boolPlaced without an account, as recorded when it was placed
getName()stringThe name of the person who placed the order. The buyer, not the recipient of the parcel.
getEmail()stringThe buyer's e-mail address
languageId()intThe language to address the buyer in. 0 when the order names none.
delivery()AddressViewThe delivery address the order was placed with. Who the parcel is for.
invoice()AddressViewThe invoice address, or the delivery address when they are the same
currency()CurrencyThe order's currency. Throws OrderCurrencyException when the order names none.
total()MoneyWhat the customer pays in total, tax included
hasComputedTotal()boolWhether total() is a calculated total and not a bare stored figure. It matters at placement only.
shipmentTotal(), shipmentTotalExcl()MoneyThe shipping cost, with and without tax
shipmentTaxRate()floatThe tax rate of the shipping cost in percent. Stated at placement for the shipping plugin that writes the parcel. 0.0 anywhere else.
shipmentChargeBasis(), shipmentTaxChain()How the shipping figures were formed. Stated at placement only, for the parcel row.
paymentMethodId(), shipmentMethodId()intThe chosen methods, 0 for none
paymentMethod()?objectThe record of the chosen payment method
items()arrayThe order's lines
payments()Payment[]Every payment, refund and authorization row of the order, in the order written
shipments()Shipment[]The order's parcels, oldest first
row()For Alfa. Do not call it.

Order::of(object $row): self makes a reader from an order row. Core uses it. An order handed to you is already a reader.

A Payment reader has id(), orderId(), methodId(), methodType(), type(), status(), transactionId(), amount() and pluginData(). A Shipment reader has id(), orderId(), methodId(), methodType(), status(), trackingNumber(), weight(), cost(), costExcl() and pluginData(). Both also have row() and of(), which are for Alfa as on the order reader.

What can go wrong​

What you seeThe cause
A property such as $order->total_shipping is emptyRead with the methods. The reader has no properties.
shipmentTaxRate() is 0.0 in a staff actionThe rate is stated only at placement. An order can hold several parcels at different rates. Read the parcel.
getName() is emptyThe order was created without a customer name, for example by an import

The total of an order​

The orders table has no column for a total. Every total is added up from the order's rows by one formula, in OrderTotalHelper:

total = lines + shipping + plugin charges - cart rules
PartWhere it comes from
LinesThe line totals of the order (#__alfa_order_items)
ShippingThe charges of the order's parcels (#__alfa_order_shipments). A cancelled or returned parcel does not count. A cart rule of the shipping class lowers this part, never below 0.
Plugin chargesThe charges plugins added in onCartTotals, stored with the order (#__alfa_order_contributions)
Cart rulesThe item and order rules stored with the order (#__alfa_order_cart_rule), each with its own tax-included figure

Changed in 2.2.0. Plugin charges are part of the total. Each stored cart rule carries its own tax-included amount; it is no longer worked out from the items' average tax.

OrderTotalHelper is core's own. A plugin reads the total from the reader ($order->total()), and a payment plugin reads what is still to collect from its base class (amountToCollect(), orderBalance()). Its public methods, so that you can follow the code:

MethodReturns
computeFromArrays(array $items, array $shipments, array $discounts, array $contributions = []): objectThe formula, on rows already loaded. Ten figures as floats: items_tax_incl, items_tax_excl, shipping_tax_incl, shipping_tax_excl, contributions_tax_incl, contributions_tax_excl, discount_tax_incl, discount_tax_excl, grand_total_tax_incl, grand_total_tax_excl.
exactFromArrays(...)The same ten figures as exact decimal texts
getBreakdown(int $orderId, ?object $order = null): objectThe same from the database, plus cart_rules and the shipping cost before shipping rules (shipping_gross_tax_incl, shipping_gross_tax_excl)
getExactBreakdown(int $orderId, ?object $order = null): objectThe ten figures from the database, as exact decimal texts
getOrderTotal(int $orderId, ?object $order = null): floatThe total, tax included
getItemsTotal(), getShippingTotal(), getDiscountTotal(), getDiscountTotalExcl()One part each. getShippingTotal() is the parcels' charges before shipping rules. Planned for 2.2.1, which is not released: getShippingTotal() is the shipping the order charges, after the shipping rules (the breakdown's shipping_tax_incl, the figure the order's total and its documents use). The charge before the rules stays in getBreakdown() as shipping_gross_tax_incl.
activeShipments(array $shipments): array, activeShipmentsWhere($db, string $prefix = ''): stringThe parcels that count: all but cancelled and returned ones

OrderStatusHelper​

Order statuses are made by the merchant. No status has a fixed id. Code that needs "the first status" or "is this order cancelled" asks this helper. It only reads, and it remembers its answers for the rest of the request.

MethodReturns
getInitialId(): ?intThe status a new order starts in. When none is marked, the first published status in the merchant's order. null when there is no status.
isInitial(?int $statusId): boolWhether this is that status
getCancelledIds(): array, isCancelled(?int $statusId): boolThe cancelled statuses. There can be several: "Cancelled", "Returned", "Refunded".
getCompletedIds(): array, isCompleted(?int $statusId): boolThe completed statuses. There can be several.
getById(?int $statusId): ?objectOne status row, or null
getAll(?string $orderBy = null): arrayEvery status row, keyed by id
clearCache(): voidForgets the remembered answers. For code that has just changed a status row.

In a listener of onOrderStatusChange you do not need it: the event has enteredCompleted(), enteredCancelled(), leftCompleted() and leftCancelled().

OrderHelper and OrderHistoryHelper​

Both are core's own and serve the order screen of the back office.

ClassPublic methodsWhat they do
OrderHelpergetOrderItems(int $orderId, ?Currency $currency = null): array, attachOrderItemFields(array $items): void, buildOrderPriceContext(int $orderId), searchProducts(string $searchTerm, PriceContext $context, int $limit = 20): array, getProductById(int $productId, PriceContext $context, int $quantity = 1): ?objectRead an order's lines with their amounts as Money, attach the field values kept on each line, and price an item for the order's customer when staff add a line. It reads only: order lines are written by the order model.
OrderHistoryHelperline(object $entry, object $order): string, details(array $context): arrayTurn one stored History entry into the sentence and the details staff read. They change nothing.

Changed in 2.2.0. OrderHelper::getOrder() and OrderHelper::setOrderItems() no longer exist.

StockPolicy​

Every order status carries a claim: what an order in that status asks of the stock. StockPolicy::claimFor() is the one place that answers it.

claimFor(int $statusId) returnsWhenWhat it means
StockPolicy::CLAIM_NONEA cancelled statusThe order holds nothing. What was taken is put back and every reservation is let go.
StockPolicy::CLAIM_RESERVATIONA status that keeps the goods in stockThe goods stay on the shelf, set aside for the order
StockPolicy::CLAIM_PHYSICALA status that removes the goods from stock, and any status Alfa does not knowThe goods are taken from stock

StockPolicy never writes a stock figure itself. It tells the stock service what to do, and the stock service announces the stock events. See What decides which of them happens.

It is core's own. OrderCreator and Transition call it for you. Its other public methods (getDefaultOrderStatus(), shouldDeductStock(), handleStatusTransition(), reconcileOrderClaim(), deductOrderStock(), restoreOrderStock()) move stock and are not for an extension.

Changed in 2.2.0. OrderStockHelper no longer exists, and nothing writes the stock column of an item directly.

The two locks​

What they are​

A lock makes a second request wait or step back, so that two requests do not do the same thing at the same moment. These two step back at once: they do not wait.

LockWhat it guardsSignature
OrderMoneyLockOne capture or release of a payment at a time, per orderOrderMoneyLock::run(int $orderId, callable $work): bool
ShipmentBookingLockOne booking with the carrier at a time, per shipmentShipmentBookingLock::run(int $shipmentId, callable $work): bool

Both run your function while holding the lock, and return false, without running it, when another request holds the lock.

When they are used​

Their class comments address plugins: a gateway for OrderMoneyLock, a carrier for ShipmentBookingLock. Core itself takes neither in 2.2.0, and the standard plugins do not take them either. ShipmentBookingLock::run() has no caller anywhere in the code. The base classes have no method that reaches them, so a plugin that uses one calls the class. There is no shipped example of either.

Your pluginWhat the class comment says
A payment plugin that captures or releases an authorized paymentOrderMoneyLock. The comment says the gateway runs its checks, the provider call and the booking inside run(). The same comment then says a gateway holds the lock around its own log write and booking, "never across a call to the provider". The two sentences do not agree, and no shipped plugin shows which is meant. This manual does not say whether the call to the provider belongs inside the lock. Planned for 2.2.1, which is not released: the comment says one thing. The gateway takes the lock with run(), checks the payment's state, records the operation it is about to send and books it, and releases the lock before any call to the provider: "the lock is never held across a call to the provider".
A shipping plugin that books a parcel with the carrierShipmentBookingLock. The comment says the carrier runs the whole booking inside run(): it reads whether the shipment is booked, asks the carrier and records the parcel.

A refund needs no such lock. A refund is set aside in the books before the provider is asked (reserveRefund()), and no lock is held while the provider is called. The class comment adds that a gateway may still hold OrderMoneyLock around its own log write and booking, never across the call to the provider. See A refund through a provider.

What you must provide​

use Alfa\Component\Alfa\Administrator\Service\Order\OrderMoneyLock;

$ran = OrderMoneyLock::run($orderId, function () use ($paymentId): void {
// The work that must not run twice for this order: check that the payment
// can still be captured, and book the result.
});

if (!$ran) {
// Another request is working on this order's money. Nothing ran. Tell staff to try again.
}

What happens next, and what can go wrong​

What they guaranteeThe same request may take the same lock again. A request that stops frees its lock. Follow-up work that Alfa records while the lock is held (an order moved by a settled payment, an e-mail) runs after the lock is released.
What they refuseAn id of 0 or less: RuntimeException. A lock the database gives no answer for: RuntimeException, and your function does not run.
What to rememberInside the lock the order has not moved yet when a payment's save() returns. Do not read the order's new status there.

Changed in 2.2.0​

  • Changed in 2.2.0. OrderStockHelper is replaced by StockPolicy. OrderHelper only reads.
  • Changed in 2.2.0. The order total includes plugin charges.
  • Since 2.2.0. Transition::expectStatus(), refusedBusy(), refusedSuperseded() and outcome().
  • Changed in 2.2.0. A status change records a follow-up step only when the step has work.
  • Changed in 2.1.4. The follow-up work of a status change is recorded with the change and carried out after the save is final.

Not documented yet​

  • Creating a complete order from an extension: OrderCreator writes the order and its lines, and nothing in 2.2.0 shows how an extension that is not the order's payment or shipping plugin adds the payment row and the parcel.
  • The draft keys customer_name, customer_email, customer_is_guest and actor_name, which the back office passes to OrderCreator::create() and the method's comment does not list.
  • The properties of the line objects that Order::items() returns.
  • The address reader (AddressView) in full.
  • Writing an entry of your own to an order's History.
  • A checked sample of OrderCreator::create(), of a direct Transition call and of either lock from an extension. No extension that ships with Alfa makes these calls, so the samples on this page were checked against the signatures and comments only.
  • Whether a shipping plugin may call Transition, and whether the call to the provider belongs inside OrderMoneyLock. The code does not settle either.
  • OrderFacts, OrderIdentity, InventoryBridge, EmailService and the other classes of administrator/src/Service/Order. They are core's own.