OrderTransitionManager
in package
uses
SourceLockTransactionTrait
Single entry point for order status changes: the admin status form, the payment hook and crons always call this, never their own UPDATE. `apply()` is the only public method.
The whole transition runs under a per-order advisory lock cms_eshop_order_{id} (GET_LOCK, 5 s,
released in finally) that serializes concurrent applies and edits. The lock is reentrant within
one session, so nested OrderStockManager calls (same lock) are safe; the lock order is
always e-shop, then store.
Idempotence layers, so that a repeated apply after a crash cannot produce a second set of effects:
- per-order lock,
- store
SourceRefidempotence (re-reserve is a no-op, confirm returns the existing posted issue document, release is a no-op), - invoice lookup by
(orderId, invoiceTypeId)withmultipleUse=falsereturns the existing invoice, - points gate
UPDATE orders SET user_is_updated=1 WHERE ... AND user_is_updated=0in the same mini transaction as crediting, - status, voucher counters,
order_payments, invoicemarkPaidand e-mails in one final DBAL transaction (transactional outbox: e-mails are rendered before it, only the INSERT runs inside).
IMPORTANT: state is written only through OrderManager::updateColumns() with a dynamic SET
list (changed columns only), never an object-graph save(), which would touch order collections
through orphanRemoval and collide with the UoW. updateColumns() runs no save hooks and uses the
same DBAL connection, so it stays inside the final transaction. The manager comes from
OrderManagerFactory, because direct injection would close a DI cycle
(OrderManager, OrderTransitionManager, OrderManager).
IMPORTANT: loyalty points are credited in their own mini transaction before the final one, not
inside it. If the process dies between them, points are credited but the status is unchanged; a
repeated apply catches the status up and does not credit points again (gate affected=0).
Known limits:
generateVoucher(VoucherGenerator) is fail-soft without a watermark: a missing level or a DB error only returns a warning (voucherLevelNotFound/voucherGenerationFailed), and unlike the invoice the cron does not retry it,- e-mails are never sent synchronously, only queued in
cms_system_email_queue.
Tags
Table of Contents
Constants
- EXPIRE_DUE_PREDICATE : string = 'o.`is_cancelled` = 0 AND o.`reservation_expire...
- Single definition of "order is due for expiry" (alias `o` = orders, `s` = their current status).
- ORDER_INCLUDES : array<string|int, mixed> = [ 'items.units', 'items.fees', 'vouchers.vouche...
- Includes for the fresh load of the order under the lock: the whole graph the transition effects need.
- UNPAID_CANCEL_DUE_PREDICATE : string = 'o.`is_cancelled` = 0 AND s.`key` = \'' . self:...
- Single definition of "unpaid order is due for cancellation" (alias `o` = orders, `s` = their current status; one `?` = cutoff of `insert_datetime`, computed by PHP because the column is written in PHP time and the DB runs in UTC). Shared by the cron selection ({@see \App\UI\Api\Eshop\Models\Managers\Traits\OrderCronTrait::cancelDueUnpaidOrderIds()}) and the check under the lock in {@see transition()} (opt `cancelUnpaidCreatedBefore`).
- UNPAID_CANCEL_STATUS_KEY : string = 'WAITING-ON-PAYMENT'
- Key of the awaiting-payment status whose orders the unpaid-cancel cron takes.
- LOCK_MAX_ATTEMPTS : int = 3
- LOCK_SCALE : int = 3
- LOCK_TIMEOUT : int = 5
- MONEY_RECEIVED_PAYMENT_STATUSES : array<string|int, mixed> = [\App\UI\Api\System\Enums\PaymentStatus::COMPLE...
- Payment statuses meaning money reached the merchant (fully, refunded, or with a wrong amount).
- ORDER_PAYMENTS_TABLE : string = 'cms_mod_eshop_order_payments'
- ORDERS_TABLE : string = 'cms_mod_eshop_orders'
- SHIPPERS_TABLE : string = 'cms_mod_eshop_shippers'
- STATUSES_TABLE : string = 'cms_mod_eshop_order_statuses'
- VOUCHER_FAILURE_SIGNAL : string = 'voucherIncrementFailed:'
- Message prefix of the `DomainException` the final transaction closure throws for an exhausted voucher.
- VOUCHERS_TABLE : string = 'cms_mod_eshop_vouchers'
Properties
- $em : EntityManagerInterface
- $emailGenerator : OrderEmailGenerator
- $emailQueueManager : EmailQueueManager
- $invoiceGenerator : OrderInvoiceGenerator
- $orderManagerFactory : OrderManagerFactory
- $orderStatusManager : OrderStatusManager
- $orderTaxManager : OrderTaxManager
- $paymentManager : PaymentManager
- $paymentRegionService : OrderPaymentRegionService
- $stockManager : OrderStockManager
- $userManager : UserManager
- $voucherGenerator : VoucherGenerator
- $voucherManager : VoucherManager
Methods
- __construct() : mixed
- apply() : Result
- Changes the order status with all effects of the target status (cancel or restore of stock and vouchers, dispatch, issue document, payment pairing, invoice, e-mails, loyalty points).
- enqueueCurrentStatusEmails() : int
- Queues the e-mail(s) of the current status without a transition, for an order that stays in its initial status after creation (`CREATED`, cash on delivery without `4/order.approval.auto`).
- acquireSourceLock() : bool
- Calls `GET_LOCK(name, timeout)` and returns true only on a definite 1 (0 = timed out or busy, null = error). `$timeout` is in seconds; 0 is non-blocking (try-lock, used by the expirer).
- connection() : Connection
- lockLogChannel() : string
- releaseSourceLock() : void
- Releases the advisory lock.
- runInSourceTransaction() : T
- Runs a closure in one raw-DBAL transaction with a deadlock retry (`SQLSTATE` 40001 / errno 1213).
- absolutePdfPath() : string|null
- Invoicer stores `pdf` as a relative path (`/www/private/...`); the e-mail attachment needs an absolute one.
- applyInMemory() : void
- Applies the intents (or, after a failed transaction, the snapshot back) to the in-memory entity, which the caller keeps using (admin form, checkout).
- checkExpeditionShipper() : Result|null
- Checks that the shipper from `opts.expedition.shipperId` exists: a pre-flight copy of the guard in {@see collectExpedition()} (which stays there as a TOCTOU safety). Nothing is checked without the key or with `null` (a dispatch without a shipper is legal).
- checkGating() : Result|null
- Checks the allowed transition: from a status that does not allow canceling one cannot go to a canceling status; from a canceled status only to a status that re-creates the order (`createOrder`). An order without a status (fresh insert) has nothing to block.
- checkVoucherCapacity() : Result|null
- Pre-checks the capacity of vouchers the transition is about to increment (restore of a canceled order). It is the same guard as the atomic `UPDATE ... WHERE max_number = 0 OR current_number < max_number` in {@see VoucherManager::incrementUsage()}, only earlier, so the reject happens before the irreversible crediting of loyalty points.
- collectExpedition() : Result|null
- Collects the dispatch columns from `$opts['expedition']` (the date defaults to now). Returns a `Result` only on error (nonexistent shipper), otherwise null with `$intents`/`$memory` filled.
- creditUserPoints() : void
- Credits loyalty points: `floor(roundedTotalPriceWithVat / currency.creditPrice)`, falling back to `roundedTotalPrice` when the total with VAT is zero. The `user_is_updated` gate is in the same mini transaction as the crediting, so points are credited exactly once even on a repeated apply.
- hasReceivedMoney() : bool
- Tells whether any payment linked to the order (`order_payments`, the module's own link) already brought money: status from the owner of `cms_system_payments` ({@see PaymentManager::getStatusRow()}).
- isExpireDue() : bool
- Checks authoritatively from the DB (under the order lock) with the same predicate as the cron selection.
- isUnpaidCancelDue() : bool
- Checks authoritatively from the DB (under the order lock) with the same predicate as the unpaid-cancel cron selection.
- ladd() : numeric-string
- lcmp() : int
- ldec() : numeric-string
- ldiv() : numeric-string
- Divides with half-up rounding to scale 3 (bcmath itself truncates; same rounding as {@see \App\UI\Api\Eshop\Models\Helpers\OrderCalculator::round3()}).
- lInt() : int
- lmul() : numeric-string
- ln() : numeric-string
- lockName() : string
- lStr() : string
- lStrN() : string|null
- lsub() : numeric-string
- optInt() : int|null
- paymentExists() : bool
- Checks payment existence through the owner of `cms_system_payments` ({@see PaymentManager::exists()}), not a raw SELECT from the e-shop. Deliberately not `find()`: it hydrates the entity (needless) and `GetAllTrait::clearDoctrine()` in the chain could detach the already loaded order graph.
- prepareEmails() : array<int, PreparedOrderEmail>
- Renders the transition e-mails before the final transaction, which then only inserts the ready rows ({@see EmailQueueManager::enqueue()}): the transactional outbox stays atomic with the status write while Latte and I/O hold no locks. An empty template, a missing address or a failing render returns null, so nothing is queued.
- reconcileFromDb() : void
- Syncs the entity with the authoritative state of the decision columns from the DB (under the lock), protecting against a stale entity from the Doctrine identity map. `em->clear()` is never called here because callers keep references to the loaded graph.
- rejectAfterStockEffects() : Result
- Compensates a rejected transition after step 5/6. The reservation made by a restore was committed by Store outside our transaction, so no rollback returns it. The compensation is needed on every reject path after step 6, not only in the catch of the final transaction, otherwise a rejected transition would leave a canceled order holding stock.
- reRateTaxAtShipping() : bool
- Re-rates VAT when dispatching an unpaid order. Unpaid means no paired payment (`order_payments`): cash on delivery and invoices with a due date are paid only after dispatch. Fail-soft: a re-rate error does not fail the dispatch (logged, the original rate stays).
- result() : Result
- shipperExists() : bool
- snapshotInMemory() : array<string, mixed>
- stringList() : array<int, string>
- Normalizes warnings from nested service Results (`getData()` is `mixed`) to a list of strings.
- transition() : Result
- undoRenewReservation() : void
- Releases the reservation made by restoring a canceled order when the transition is rejected.
- voucherIds() : array<int, int>
- Returns ids of vouchers applied to the order (only those linked to a live voucher; a snapshot without an FK has no counter to give back to).
- writeOrderColumns() : void
- Writes the intents through {@see OrderManager::updateColumns()} with a dynamic SET list: only the columns the transition really changes (never a full object-graph save, which would endanger order collections through orphanRemoval). `$intents` keys are `Order` entity properties; the mapper translates them to columns (also FKs of ToOne associations such as `status`/`payment`/ `paymentRegion`/`expeditionShipper`, where the value is the id). `updateColumns()` runs no save hooks and uses the same connection, so it stays inside the caller's final transaction.
Constants
EXPIRE_DUE_PREDICATE
Single definition of "order is due for expiry" (alias `o` = orders, `s` = their current status).
public
string
EXPIRE_DUE_PREDICATE
= 'o.`is_cancelled` = 0 AND o.`reservation_expires_at` IS NOT NULL' . ' AND o.`reservation_expires_at` < NOW() AND s.`allow_cancelling` = 1'
Shared by the cron selection (OrderCronTrait::expireDueOrderIds())
and the check under the lock in transition() (opt expireOnlyIfDue), so they cannot diverge.
ORDER_INCLUDES
Includes for the fresh load of the order under the lock: the whole graph the transition effects need.
public
array<string|int, mixed>
ORDER_INCLUDES
= [
'items.units',
'items.fees',
'vouchers.voucher',
'gifts',
'status.translations',
'currency',
'user',
'language',
// Amount and date format in customer e-mails (`OrderEmailGenerator::inOrderFormat()`).
'localization',
'paymentRegion',
'shipper',
'paymentMethod',
'payments',
'expeditionShipper',
]
IMPORTANT: must contain every collection touched by anything run from apply(), notably
OrderInvoiceGenerator::appendLines(). A collection left lazy would be initialized inside the
invoice generator, after applySeller(), whose findAll() runs GetAllTrait::clearDoctrine() and
detaches the whole identity map. The collection then hydrates with a detached owner and the invoice
flush fails with "A new entity was found through the relationship", so the invoice is never issued.
Eager includes hydrate the collection before anything can clear the identity map.
UNPAID_CANCEL_DUE_PREDICATE
Single definition of "unpaid order is due for cancellation" (alias `o` = orders, `s` = their current status; one `?` = cutoff of `insert_datetime`, computed by PHP because the column is written in PHP time and the DB runs in UTC). Shared by the cron selection ({@see \App\UI\Api\Eshop\Models\Managers\Traits\OrderCronTrait::cancelDueUnpaidOrderIds()}) and the check under the lock in {@see transition()} (opt `cancelUnpaidCreatedBefore`).
public
string
UNPAID_CANCEL_DUE_PREDICATE
= 'o.`is_cancelled` = 0 AND s.`key` = \'' . self::UNPAID_CANCEL_STATUS_KEY . '\'' . ' AND o.`insert_datetime` < ?'
UNPAID_CANCEL_STATUS_KEY
Key of the awaiting-payment status whose orders the unpaid-cancel cron takes.
public
string
UNPAID_CANCEL_STATUS_KEY
= 'WAITING-ON-PAYMENT'
LOCK_MAX_ATTEMPTS
private
int
LOCK_MAX_ATTEMPTS
= 3
LOCK_SCALE
private
int
LOCK_SCALE
= 3
LOCK_TIMEOUT
private
int
LOCK_TIMEOUT
= 5
MONEY_RECEIVED_PAYMENT_STATUSES
Payment statuses meaning money reached the merchant (fully, refunded, or with a wrong amount).
private
array<string|int, mixed>
MONEY_RECEIVED_PAYMENT_STATUSES
= [\App\UI\Api\System\Enums\PaymentStatus::COMPLETED, \App\UI\Api\System\Enums\PaymentStatus::REFUNDED, \App\UI\Api\System\Enums\PaymentStatus::BAD_PRICE]
An order with such a linked payment is never cancelled for non-payment; the admin settles it.
ORDER_PAYMENTS_TABLE
private
string
ORDER_PAYMENTS_TABLE
= 'cms_mod_eshop_order_payments'
ORDERS_TABLE
private
string
ORDERS_TABLE
= 'cms_mod_eshop_orders'
SHIPPERS_TABLE
private
string
SHIPPERS_TABLE
= 'cms_mod_eshop_shippers'
STATUSES_TABLE
private
string
STATUSES_TABLE
= 'cms_mod_eshop_order_statuses'
VOUCHER_FAILURE_SIGNAL
Message prefix of the `DomainException` the final transaction closure throws for an exhausted voucher.
private
string
VOUCHER_FAILURE_SIGNAL
= 'voucherIncrementFailed:'
VOUCHERS_TABLE
private
string
VOUCHERS_TABLE
= 'cms_mod_eshop_vouchers'
Properties
$em read-only
private
EntityManagerInterface
$em
$emailGenerator read-only
private
OrderEmailGenerator
$emailGenerator
$emailQueueManager read-only
private
EmailQueueManager
$emailQueueManager
$invoiceGenerator read-only
private
OrderInvoiceGenerator
$invoiceGenerator
$orderManagerFactory read-only
private
OrderManagerFactory
$orderManagerFactory
$orderStatusManager read-only
private
OrderStatusManager
$orderStatusManager
$orderTaxManager read-only
private
OrderTaxManager
$orderTaxManager
$paymentManager read-only
private
PaymentManager
$paymentManager
$paymentRegionService read-only
private
OrderPaymentRegionService
$paymentRegionService
$stockManager read-only
private
OrderStockManager
$stockManager
$userManager read-only
private
UserManager
$userManager
$voucherGenerator read-only
private
VoucherGenerator
$voucherGenerator
$voucherManager read-only
private
VoucherManager
$voucherManager
Methods
__construct()
public
__construct(EntityManagerInterface $em, OrderManagerFactory $orderManagerFactory, OrderStatusManager $orderStatusManager, VoucherManager $voucherManager, OrderStockManager $stockManager, OrderInvoiceGenerator $invoiceGenerator, OrderEmailGenerator $emailGenerator, OrderPaymentRegionService $paymentRegionService, VoucherGenerator $voucherGenerator, OrderTaxManager $orderTaxManager, PaymentManager $paymentManager, UserManager $userManager, EmailQueueManager $emailQueueManager) : mixed
Parameters
- $em : EntityManagerInterface
- $orderManagerFactory : OrderManagerFactory
- $orderStatusManager : OrderStatusManager
- $voucherManager : VoucherManager
- $stockManager : OrderStockManager
- $invoiceGenerator : OrderInvoiceGenerator
- $emailGenerator : OrderEmailGenerator
- $paymentRegionService : OrderPaymentRegionService
- $voucherGenerator : VoucherGenerator
- $orderTaxManager : OrderTaxManager
- $paymentManager : PaymentManager
- $userManager : UserManager
- $emailQueueManager : EmailQueueManager
apply()
Changes the order status with all effects of the target status (cancel or restore of stock and vouchers, dispatch, issue document, payment pairing, invoice, e-mails, loyalty points).
public
apply(int $orderId, int $newStatusId[, array<string, mixed> $opts = [] ]) : Result
$opts:
statusInformations(string): status note, stored instatus_informations,paymentId(int): paired System payment (only withstatus.pairPayment); optional because cash on delivery has none, but without it invoices are not marked paid and the warningpaymentNotPairedis returned,expedition(array{datetime?: DateTimeInterface, shipperId?: int, branchId?: int, code?: string, informations?: string}): only withstatus.fillExpedition,sendStatusEmail/sendDefaultEmail(bool, default true): disable a specific e-mail,sendExpeditionEmail(bool, default perShipper::getSendEmail()),generateInvoice(bool, default true): disables invoice issuing (parity with the admin checkbox),allowPartial(bool, default false = strict): only for restoring a canceled order,userId(int): who performed the transition (goes to the store issue document),expireOnlyIfDue(bool, default false): cron path. After the fresh load under the lock and before any effect (store release, vouchers, status write) EXPIRE_DUE_PREDICATE is checked again. If the admin meanwhile moved, canceled or extended the order, a no-op success (noop=true) is returned, not an error. Manual transitions do not send it.cancelUnpaidCreatedBefore(DateTimeInterface): cron path of the unpaid-order cancellation. Under the lock UNPAID_CANCEL_DUE_PREDICATE is checked with this cutoff (not due = no-op success); a linked payment with money received rejects withpaymentReceived. Only then the gate "status does not allow cancelling" is lifted for this one transition (the awaiting-payment status hasallow_cancelling=0, so the admin still cannot cancel it by hand); the target must cancel the order (cancelOrder=1), otherwisetransitionNotAllowed.
Success data: ['noop'=>bool,'warnings'=>list<string>,'invoiceId'=>?int, 'stockDocumentId'=>?int]. Failure errorCode in data: nestedTransaction, orderLocked,
orderNotFound, statusNotFound, transitionNotAllowed, renewReserveFailed,
stockConfirmFailed, paymentNotFound, paymentReceived, shipperNotFound, voucherIncrementFailed,
transactionFailed, transitionFailed (unexpected exception outside the final transaction;
the method never throws, see the catch below).
IMPORTANT: never call inside an open transaction (the guard below rejects it as
nestedTransaction). In DBAL 4 the final transaction would become only a savepoint of the
outer one, so the e-mail outbox and the status write would lose durability (an outer rollback
silently takes them) while Store and Invoicer already committed on their own. This applies to
the payment hook too: the payment callback must commit its transaction first and call
apply() after that.
Parameters
- $orderId : int
- $newStatusId : int
- $opts : array<string, mixed> = []
Return values
ResultenqueueCurrentStatusEmails()
Queues the e-mail(s) of the current status without a transition, for an order that stays in its initial status after creation (`CREATED`, cash on delivery without `4/order.approval.auto`).
public
enqueueCurrentStatusEmails(TOrderGraph $order, TOrderStatusGraph $status, string|null $invoicePdf[, string|null $invoiceVariableSymbol = null ]) : int
apply() to the same status is a no-op (step 3), so such an order would get no confirmation
although the status has send_email=1 and its own text. Rendering and rules are the same as for
a transition (prepareEmails(): the status's own text, ES_ORDER_CHANGED only as a
fallback); the queue write runs in its own mini transaction. Fail-soft: returns the number of
queued e-mails, an error is logged and returned as 0.
Parameters
- $order : TOrderGraph
- $status : TOrderStatusGraph
- $invoicePdf : string|null
-
relative path of the invoice PDF (as the generator returns it), or null
- $invoiceVariableSymbol : string|null = null
Return values
intacquireSourceLock()
Calls `GET_LOCK(name, timeout)` and returns true only on a definite 1 (0 = timed out or busy, null = error). `$timeout` is in seconds; 0 is non-blocking (try-lock, used by the expirer).
protected
acquireSourceLock(string $name, int $timeout) : bool
Parameters
- $name : string
- $timeout : int
Return values
boolconnection()
protected
connection() : Connection
Return values
ConnectionlockLogChannel()
protected
lockLogChannel() : string
Return values
stringreleaseSourceLock()
Releases the advisory lock.
protected
releaseSourceLock(string $name) : void
IMPORTANT: it is called from a finally, so a failure here (a gone-away connection after a
fatal query) is swallowed and logged; rethrowing would mask the original exception being
unwound. MySQL drops session locks when the connection dies anyway, so nothing leaks past
the process.
Parameters
- $name : string
runInSourceTransaction()
Runs a closure in one raw-DBAL transaction with a deadlock retry (`SQLSTATE` 40001 / errno 1213).
protected
runInSourceTransaction(callable(Connection): T $fn) : T
The advisory lock (held by the caller across this call) is not released by a rollback, so every attempt runs under the same serialization.
Parameters
- $fn : callable(Connection): T
Tags
Return values
TabsolutePdfPath()
Invoicer stores `pdf` as a relative path (`/www/private/...`); the e-mail attachment needs an absolute one.
private
absolutePdfPath(mixed $pdf) : string|null
Parameters
- $pdf : mixed
Return values
string|nullapplyInMemory()
Applies the intents (or, after a failed transaction, the snapshot back) to the in-memory entity, which the caller keeps using (admin form, checkout).
private
applyInMemory(Order $order, array<string, mixed> $memory) : void
Parameters
- $order : Order
- $memory : array<string, mixed>
checkExpeditionShipper()
Checks that the shipper from `opts.expedition.shipperId` exists: a pre-flight copy of the guard in {@see collectExpedition()} (which stays there as a TOCTOU safety). Nothing is checked without the key or with `null` (a dispatch without a shipper is legal).
private
checkExpeditionShipper(array<string, mixed> $opts, int $orderId) : Result|null
Parameters
- $opts : array<string, mixed>
- $orderId : int
Return values
Result|nullcheckGating()
Checks the allowed transition: from a status that does not allow canceling one cannot go to a canceling status; from a canceled status only to a status that re-creates the order (`createOrder`). An order without a status (fresh insert) has nothing to block.
private
checkGating(OrderStatus|null $current, OrderStatus $target[, bool $unpaidCancel = false ]) : Result|null
$unpaidCancel lifts only the first rule, for the cron cancellation of an unpaid order already
verified as due under the lock (opt cancelUnpaidCreatedBefore).
Parameters
- $current : OrderStatus|null
- $target : OrderStatus
- $unpaidCancel : bool = false
Return values
Result|nullcheckVoucherCapacity()
Pre-checks the capacity of vouchers the transition is about to increment (restore of a canceled order). It is the same guard as the atomic `UPDATE ... WHERE max_number = 0 OR current_number < max_number` in {@see VoucherManager::incrementUsage()}, only earlier, so the reject happens before the irreversible crediting of loyalty points.
private
checkVoucherCapacity(array<int, int> $voucherIds, int $orderId) : Result|null
NOTE: capacity can still vanish between this pre-check and the final transaction (another order
uses the voucher up). Then only the in-transaction increment rejects and the points for the
rejected transition stay credited; the window is just the short time between pre-check and commit.
Removing it would need the points inside the final transaction, which is deliberately avoided: the
user_is_updated gate must be committed even if the process dies, so points are not credited twice.
Parameters
- $voucherIds : array<int, int>
- $orderId : int
Return values
Result|nullcollectExpedition()
Collects the dispatch columns from `$opts['expedition']` (the date defaults to now). Returns a `Result` only on error (nonexistent shipper), otherwise null with `$intents`/`$memory` filled.
private
collectExpedition(Order $order, array<string, mixed> $opts, array<string, mixed> &$intents, array<string, mixed> &$memory) : Result|null
Parameters
- $order : Order
- $opts : array<string, mixed>
- $intents : array<string, mixed>
- $memory : array<string, mixed>
Return values
Result|nullcreditUserPoints()
Credits loyalty points: `floor(roundedTotalPriceWithVat / currency.creditPrice)`, falling back to `roundedTotalPrice` when the total with VAT is zero. The `user_is_updated` gate is in the same mini transaction as the crediting, so points are credited exactly once even on a repeated apply.
private
creditUserPoints(Order $order, int $orderId, array<int, string> &$warnings) : void
Parameters
- $order : Order
- $orderId : int
- $warnings : array<int, string>
hasReceivedMoney()
Tells whether any payment linked to the order (`order_payments`, the module's own link) already brought money: status from the owner of `cms_system_payments` ({@see PaymentManager::getStatusRow()}).
private
hasReceivedMoney(int $orderId) : bool
Parameters
- $orderId : int
Return values
boolisExpireDue()
Checks authoritatively from the DB (under the order lock) with the same predicate as the cron selection.
private
isExpireDue(int $orderId) : bool
Parameters
- $orderId : int
Return values
boolisUnpaidCancelDue()
Checks authoritatively from the DB (under the order lock) with the same predicate as the unpaid-cancel cron selection.
private
isUnpaidCancelDue(int $orderId, DateTimeInterface $createdBefore) : bool
Parameters
- $orderId : int
- $createdBefore : DateTimeInterface
Return values
boolladd()
private
ladd(string $a, string $b) : numeric-string
Parameters
- $a : string
- $b : string
Return values
numeric-stringlcmp()
private
lcmp(string $a, string $b) : int
Parameters
- $a : string
- $b : string
Return values
intldec()
private
ldec(string $value) : numeric-string
Parameters
- $value : string
Return values
numeric-stringldiv()
Divides with half-up rounding to scale 3 (bcmath itself truncates; same rounding as {@see \App\UI\Api\Eshop\Models\Helpers\OrderCalculator::round3()}).
private
ldiv(string $a, string $b) : numeric-string
IMPORTANT: truncation would systematically drop up to 0.001 from every unit on large quantities, so the invariant sum(unitPrice x quantity) ~ sum(rowBase) would drift in one direction (an issue document undervalued against the order).
Parameters
- $a : string
- $b : string
Return values
numeric-stringlInt()
private
lInt(mixed $value) : int
Parameters
- $value : mixed
Return values
intlmul()
private
lmul(string $a, string $b) : numeric-string
Parameters
- $a : string
- $b : string
Return values
numeric-stringln()
private
ln(string $value) : numeric-string
Parameters
- $value : string
Return values
numeric-stringlockName()
private
lockName(int $orderId) : string
Parameters
- $orderId : int
Return values
stringlStr()
private
lStr(mixed $value) : string
Parameters
- $value : mixed
Return values
stringlStrN()
private
lStrN(mixed $value) : string|null
Parameters
- $value : mixed
Return values
string|nulllsub()
private
lsub(string $a, string $b) : numeric-string
Parameters
- $a : string
- $b : string
Return values
numeric-stringoptInt()
private
optInt(array<string, mixed> $opts, string $key) : int|null
Parameters
- $opts : array<string, mixed>
- $key : string
Return values
int|nullpaymentExists()
Checks payment existence through the owner of `cms_system_payments` ({@see PaymentManager::exists()}), not a raw SELECT from the e-shop. Deliberately not `find()`: it hydrates the entity (needless) and `GetAllTrait::clearDoctrine()` in the chain could detach the already loaded order graph.
private
paymentExists(int $paymentId) : bool
Parameters
- $paymentId : int
Return values
boolprepareEmails()
Renders the transition e-mails before the final transaction, which then only inserts the ready rows ({@see EmailQueueManager::enqueue()}): the transactional outbox stays atomic with the status write while Latte and I/O hold no locks. An empty template, a missing address or a failing render returns null, so nothing is queued.
private
prepareEmails(TOrderGraph $order, TOrderStatusGraph $target, array<string, mixed> $opts, string|null $invoicePdfPath, Voucher|null $generatedVoucher, array{voucherLevelId: int, durationDays: int, minPrice: string, maxPrice: string, percents: int, fixedAmount: ?string}|null $generatedVoucherLevel[, string|null $invoiceVariableSymbol = null ]) : array<int, PreparedOrderEmail>
Call only after the intents were applied to the entity (the dispatch notice reads the target
status name from order.status).
Parameters
- $order : TOrderGraph
- $target : TOrderStatusGraph
- $opts : array<string, mixed>
- $invoicePdfPath : string|null
- $generatedVoucher : Voucher|null
- $generatedVoucherLevel : array{voucherLevelId: int, durationDays: int, minPrice: string, maxPrice: string, percents: int, fixedAmount: ?string}|null
- $invoiceVariableSymbol : string|null = null
Return values
array<int, PreparedOrderEmail>reconcileFromDb()
Syncs the entity with the authoritative state of the decision columns from the DB (under the lock), protecting against a stale entity from the Doctrine identity map. `em->clear()` is never called here because callers keep references to the loaded graph.
private
reconcileFromDb(Order $order, int $orderId) : void
Parameters
- $order : Order
- $orderId : int
rejectAfterStockEffects()
Compensates a rejected transition after step 5/6. The reservation made by a restore was committed by Store outside our transaction, so no rollback returns it. The compensation is needed on every reject path after step 6, not only in the catch of the final transaction, otherwise a rejected transition would leave a canceled order holding stock.
private
rejectAfterStockEffects(Result $failure, TOrderGraph $order, bool $renewReserved) : Result
Parameters
- $failure : Result
- $order : TOrderGraph
- $renewReserved : bool
Return values
ResultreRateTaxAtShipping()
Re-rates VAT when dispatching an unpaid order. Unpaid means no paired payment (`order_payments`): cash on delivery and invoices with a due date are paid only after dispatch. Fail-soft: a re-rate error does not fail the dispatch (logged, the original rate stays).
private
reRateTaxAtShipping(TOrderGraph $order, int $orderId, array<string, mixed> $memory) : bool
Parameters
- $order : TOrderGraph
- $orderId : int
- $memory : array<string, mixed>
-
in-memory intents (the dispatch date is already in them)
Return values
boolresult()
private
result(bool $noop, array<int, string> $warnings, int|null $invoiceId, int|null $stockDocumentId) : Result
Parameters
- $noop : bool
- $warnings : array<int, string>
- $invoiceId : int|null
- $stockDocumentId : int|null
Return values
ResultshipperExists()
private
shipperExists(int $shipperId) : bool
Parameters
- $shipperId : int
Return values
boolsnapshotInMemory()
private
snapshotInMemory(Order $order) : array<string, mixed>
Parameters
- $order : Order
Return values
array<string, mixed>stringList()
Normalizes warnings from nested service Results (`getData()` is `mixed`) to a list of strings.
private
stringList(mixed $value) : array<int, string>
Parameters
- $value : mixed
Return values
array<int, string>transition()
private
transition(int $orderId, int $newStatusId, array<string, mixed> $opts) : Result
Parameters
- $orderId : int
- $newStatusId : int
- $opts : array<string, mixed>
Return values
ResultundoRenewReservation()
Releases the reservation made by restoring a canceled order when the transition is rejected.
private
undoRenewReservation(Order $order, bool $renewReserved) : void
Store committed it in its own transaction before the final one, so a rollback does not return it, and a canceled order must not keep holding stock.
Parameters
- $order : Order
- $renewReserved : bool
voucherIds()
Returns ids of vouchers applied to the order (only those linked to a live voucher; a snapshot without an FK has no counter to give back to).
private
voucherIds(Order $order) : array<int, int>
Parameters
- $order : Order
Return values
array<int, int>writeOrderColumns()
Writes the intents through {@see OrderManager::updateColumns()} with a dynamic SET list: only the columns the transition really changes (never a full object-graph save, which would endanger order collections through orphanRemoval). `$intents` keys are `Order` entity properties; the mapper translates them to columns (also FKs of ToOne associations such as `status`/`payment`/ `paymentRegion`/`expeditionShipper`, where the value is the id). `updateColumns()` runs no save hooks and uses the same connection, so it stays inside the caller's final transaction.
private
writeOrderColumns(int $orderId, array<string, mixed> $intents) : void
IMPORTANT: the repository swallows a DBAL exception in updateColumns() and returns
Result::failure. Inside a transaction that would mean a silent partial transition (status not
written but e-mails and voucher counters committed), so the failure is turned back into an
exception: the catch of the final transaction rolls back everything and returns transactionFailed.
Parameters
- $orderId : int
- $intents : array<string, mixed>