OrderCreditNoteManager
in package
uses
SourceLockTransactionTrait
Issues a partial credit note as a standalone action on an invoice.
This is not a state transition. A whole-order credit note is born from the transition to state 9 ("returned"); routing a partial one the same way would make the state machine lie, because returning 1 of 3 items does not make the order returned. Hence a separate manager-tier entry point and its own RBAC key.
Both paths share the limit and the lock:
- the limit is enforced by OrderInvoiceGenerator::buildCreditNoteWithinLimit() inside the generator, so no caller can bypass it;
- the lock is the same per-order advisory lock as in OrderTransitionManager::apply()
(
cms_eshop_order_{id}). Without it two concurrent requests would read the same remainder, both pass the limit and credit the goods twice.
The lock is taken here, not in the generator: the generator also runs under the transition
lock, so the lock belongs to the entry point that owns the operation. A nested GET_LOCK of
the same name on one session is counted (MariaDB keeps it until every RELEASE_LOCK), so
nesting would be harmless, just redundant.
Table of Contents
Constants
- LOCK_MAX_ATTEMPTS : int = 3
- LOCK_SCALE : int = 3
- LOCK_TIMEOUT : int = 5
Properties
- $complaintManager : ComplaintManager
- $creditNoteLedger : CreditNoteLedger
- $em : EntityManagerInterface
- $invoiceGenerator : OrderInvoiceGenerator
- $orderManagerFactory : OrderManagerFactory
Methods
- __construct() : mixed
- issuePartial() : Result
- Issues a partial credit note for selected lines of the order's source invoice.
- preview() : Result
- Returns the data for the partial credit note form: source invoice lines with label, invoiced and already credited quantity and the remainder.
- 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).
- 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
- normalizeLines() : array<string, array<int, int>>
- Drops zero/negative quantities and foreign collection keys.
- prefillFromComplaint() : array<string, array<int, int>>
- Prefills the form from the approved complaint items.
Constants
LOCK_MAX_ATTEMPTS
private
int
LOCK_MAX_ATTEMPTS
= 3
LOCK_SCALE
private
int
LOCK_SCALE
= 3
LOCK_TIMEOUT
private
int
LOCK_TIMEOUT
= 5
Properties
$complaintManager read-only
private
ComplaintManager
$complaintManager
$creditNoteLedger read-only
private
CreditNoteLedger
$creditNoteLedger
$em read-only
private
EntityManagerInterface
$em
$invoiceGenerator read-only
private
OrderInvoiceGenerator
$invoiceGenerator
$orderManagerFactory read-only
private
OrderManagerFactory
$orderManagerFactory
Methods
__construct()
public
__construct(EntityManagerInterface $em, OrderManagerFactory $orderManagerFactory, OrderInvoiceGenerator $invoiceGenerator, CreditNoteLedger $creditNoteLedger, ComplaintManager $complaintManager) : mixed
Parameters
- $em : EntityManagerInterface
- $orderManagerFactory : OrderManagerFactory
- $invoiceGenerator : OrderInvoiceGenerator
- $creditNoteLedger : CreditNoteLedger
- $complaintManager : ComplaintManager
issuePartial()
Issues a partial credit note for selected lines of the order's source invoice.
public
issuePartial(int $orderId, array<string, array<int, int>> $requestedLines) : Result
Always returns a Result; the admin form builds its flash messages on it.
Failure errorCode: orderLocked, nestedTransaction, orderNotFound, noLinesSelected,
plus permanent ones from the generator (sourceInvoiceMissing, unattributedCreditLines,
nothingToCredit, creditLimitExceeded, creditNoteTypeMissing,
decimalQuantityUnsupported).
IMPORTANT: decimalQuantityUnsupported arrives as a failed Result, not as an exception;
OrderInvoiceGenerator::buildCreditNoteWithinLimit()
converts it at the ledger. Otherwise the generic catch below would swallow it as an
unexpected error. The conversion lives in the generator because the whole-order credit
note (state transition, cron) goes through the same place. preview() keeps its own
equivalent catch since it calls the ledger directly.
Parameters
- $orderId : int
- $requestedLines : array<string, array<int, int>>
-
collection => (source invoice line id => pieces)
Return values
Resultpreview()
Returns the data for the partial credit note form: source invoice lines with label, invoiced and already credited quantity and the remainder.
public
preview(int $orderId[, int|null $complaintId = null ]) : Result
The source invoice comes from OrderInvoiceGenerator::findCreditNoteSource(), the same method the credit note is really built from; a separate "latest invoice" lookup would drift and the UI would offer lines of another document.
No lock (read only). The remainder may change before the submit; the authoritative check is the one in issuePartial() under the lock.
Failure errorCode: sourceInvoiceMissing, decimalQuantityUnsupported (the source
invoice has a line with a decimal quantity; the credit limit supports whole pieces only).
Parameters
- $orderId : int
- $complaintId : int|null = null
Return values
Result —data: array{sourceInvoiceId: int, sourceInvoiceNumber: ?string, lines: array<string, list<array<string, mixed>>>, hasRemainder: bool, blocked: bool}
acquireSourceLock()
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
Tladd()
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-stringnormalizeLines()
Drops zero/negative quantities and foreign collection keys.
private
normalizeLines(array<string, array<int, int>> $lines) : array<string, array<int, int>>
The limit itself is enforced by CreditNoteLedger::violations(); this only cleans the form input so an empty "return" field does not send zeros that look like an attempt at an empty credit note.
Parameters
- $lines : array<string, array<int, int>>
Return values
array<string, array<int, int>>prefillFromComplaint()
Prefills the form from the approved complaint items.
private
prefillFromComplaint(int $complaintId, array<string, array<int, array<string, mixed>>> $lines) : array<string, array<int, int>>
A credit note from a complaint is issued manually; this is only a suggested quantity that the admin sees and confirms. A complaint does not lower the limit: the suggestion is clipped to the ledger remainder and never exceeds it.
Mapping goes through invoice_items.order_item_id. IMPORTANT: it is not unique, an item fee
has its own invoice line with the same order_item_id, so both lines get prefilled
(returning goods also returns the packaging fee for those pieces). The admin can reduce
them before confirming.
Parameters
- $complaintId : int
- $lines : array<string, array<int, array<string, mixed>>>
-
lines from preview()
Return values
array<string, array<int, int>> —collection => (invoice line id => suggested pieces)