CreditNoteLedger
in package
Remaining balances for partial credit notes.
Holds the single interpretation of the invariant that for every line of the source invoice the sum of credited pieces is at most the invoiced pieces. Full-order and partial credit notes both pass through this sum, otherwise one path would bypass the cap of the other.
- The remainder is recomputed on every call, never kept as a counter (a counter drifts after the
first deleted document). The source of truth is the existing credit notes (
Invoice::relatedInvoiceId) and theirrelated_*_idlines. - The key is the id of the source invoice line, never
order_item_id: that one is not unique, because item fees get their own invoice line with the sameorder_item_id. - Vouchers are keyed by amount, not pieces. A voucher on an e-shop invoice is always a fixed amount
(
percents = null), so a partial credit note must prorate it and the ledger sums how much was already dissolved. A percentage voucher (manual invoices only) dissolves itself by copyingpercents. unattributedcounts credit note lines withoutrelated_*_id(old documents the backfill could not pair). The cap cannot be trusted for such an invoice, so callers must reject it, not ignore it.- Quantities are always whole pieces.
InvoiceItem::quantityisdecimal(8,2), but the ledger,violations()and credit note line mirroring work withint. A fractional quantity is never truncated ((int) '2.50'would lock the remaining 0.5 forever, and 0.50 would drop the line from the ledger); it throws instead. Credit notes over fractional quantities are manual work.
The ledger takes no lock: the caller must run the cap check and the credit note write inside
acquireSourceLock().
Reads are live: InvoiceService is not in the wire-payload query cache allowlist
(cache.config.enabledServices), so the ledger always hits the database.
IMPORTANT: the cap is a monetary check; re-verify that assumption before enabling the query cache
for Invoicer, otherwise a stale ledger lets a line be credited twice.
Tags
Table of Contents
Constants
- COLLECTION_ITEMS : string = 'items'
- Collections that carry a quantity; they have a per-piece cap.
- COLLECTION_PAYMENT_METHODS : string = 'paymentMethods'
- COLLECTION_SHIPPERS : string = 'shippers'
- LINE_INCLUDES : array<string|int, mixed> = ['items', 'shippers', 'paymentMethods', 'vouche...
- MONEY_SCALE : int = 3
- Scale of the Invoicer `decimal(10,3)` columns, same as `OrderInvoiceGenerator`.
- QUANTITY_COLLECTIONS : array<string|int, mixed> = [self::COLLECTION_ITEMS, self::COLLECTION_SHIPP...
Properties
Methods
- __construct() : mixed
- forSource() : TCreditNoteLedger
- Builds the ledger from an already loaded source invoice, saving a query for callers that hold it (see {@see \App\UI\Api\Eshop\Models\Generators\OrderInvoiceGenerator}).
- forSourceInvoiceId() : TCreditNoteLedger|null
- Builds the ledger by source invoice id; returns null when the invoice does not exist.
- hasRemainder() : bool
- Tells whether anything is left to credit on the source invoice (the UI offers the button only then).
- hasUnattributedCreditLines() : bool
- Tells whether the source has a credit note line that cannot be attributed.
- violations() : array<int, array{collection: string, sourceLineId: int, requested: int, remaining: int}>
- Returns the cap violations of the requested partial credit note; an empty array means it is fine.
-
creditedQuantities()
: array{credited: array
, unattributed: int} - Returns the quantity already credited by the lines of one credit note, keyed by source line id.
-
creditedVoucherAmounts()
: array{credited: array
, unattributed: int} - Returns the fixed amount already dissolved by the vouchers of one credit note.
- creditNotesOf() : array<int, Invoice>
- Returns the credit notes of the source invoice.
- invoicedQuantities() : array<int, TLedgerLine>
- Returns the invoiced quantity per source line.
- invoicedVoucherAmounts() : array<int, TLedgerVoucher>
- Returns the fixed discount amount per source voucher.
- money() : numeric-string
- relatedLineId() : int|null
- wholeQuantity() : int
- Returns the line quantity as whole pieces and fails loudly otherwise.
- withRemainders() : TCreditNoteLedger
- Computes `remaining`, clamped to 0 so an overdrawn line (corrupt data) cannot lend a negative balance; the cap just closes.
Constants
COLLECTION_ITEMS
Collections that carry a quantity; they have a per-piece cap.
public
string
COLLECTION_ITEMS
= 'items'
COLLECTION_PAYMENT_METHODS
public
string
COLLECTION_PAYMENT_METHODS
= 'paymentMethods'
COLLECTION_SHIPPERS
public
string
COLLECTION_SHIPPERS
= 'shippers'
LINE_INCLUDES
private
array<string|int, mixed>
LINE_INCLUDES
= ['items', 'shippers', 'paymentMethods', 'vouchers']
MONEY_SCALE
Scale of the Invoicer `decimal(10,3)` columns, same as `OrderInvoiceGenerator`.
private
int
MONEY_SCALE
= 3
QUANTITY_COLLECTIONS
private
array<string|int, mixed>
QUANTITY_COLLECTIONS
= [self::COLLECTION_ITEMS, self::COLLECTION_SHIPPERS, self::COLLECTION_PAYMENT_METHODS]
Properties
$invoiceManager read-only
private
InvoiceManager
$invoiceManager
Methods
__construct()
public
__construct(InvoiceManager $invoiceManager) : mixed
Parameters
- $invoiceManager : InvoiceManager
forSource()
Builds the ledger from an already loaded source invoice, saving a query for callers that hold it (see {@see \App\UI\Api\Eshop\Models\Generators\OrderInvoiceGenerator}).
public
forSource(Invoice $source) : TCreditNoteLedger
IMPORTANT: the invoice must be loaded with the includes LINE_INCLUDES. Otherwise lazy empty
collections look like nothing was invoiced and the cap lets everything through.
Parameters
- $source : Invoice
Tags
Return values
TCreditNoteLedgerforSourceInvoiceId()
Builds the ledger by source invoice id; returns null when the invoice does not exist.
public
forSourceInvoiceId(int $sourceInvoiceId) : TCreditNoteLedger|null
Parameters
- $sourceInvoiceId : int
Tags
Return values
TCreditNoteLedger|nullhasRemainder()
Tells whether anything is left to credit on the source invoice (the UI offers the button only then).
public
hasRemainder(TCreditNoteLedger $ledger) : bool
Parameters
- $ledger : TCreditNoteLedger
Return values
boolhasUnattributedCreditLines()
Tells whether the source has a credit note line that cannot be attributed.
public
hasUnattributedCreditLines(TCreditNoteLedger $ledger) : bool
The cap does not hold then and a partial credit note must be rejected; only a person can pair it.
Parameters
- $ledger : TCreditNoteLedger
Return values
boolviolations()
Returns the cap violations of the requested partial credit note; an empty array means it is fine.
public
violations(TCreditNoteLedger $ledger, array<string, array<int, int>> $requested) : array<int, array{collection: string, sourceLineId: int, requested: int, remaining: int}>
This is the only place where the cap is evaluated. Call it inside the source lock.
Parameters
- $ledger : TCreditNoteLedger
- $requested : array<string, array<int, int>>
-
collection => (source line id => pieces)
Return values
array<int, array{collection: string, sourceLineId: int, requested: int, remaining: int}>creditedQuantities()
Returns the quantity already credited by the lines of one credit note, keyed by source line id.
private
creditedQuantities(iterable<string|int, mixed> $lines) : array{credited: array, unattributed: int}
The quantity on a credit note is positive (quantity is mirrored, the price is negated; see mirrorLine()).
Parameters
- $lines : iterable<string|int, mixed>
Tags
Return values
array{credited: arraycreditedVoucherAmounts()
Returns the fixed amount already dissolved by the vouchers of one credit note.
private
creditedVoucherAmounts(iterable<string|int, mixed> $vouchers) : array{credited: array, unattributed: int}
The price is negated on a credit note, so it is returned as a positive magnitude to sum
with the same sign as invoiced.
Parameters
- $vouchers : iterable<string|int, mixed>
Return values
array{credited: arraycreditNotesOf()
Returns the credit notes of the source invoice.
private
creditNotesOf(int $sourceInvoiceId) : array<int, Invoice>
There is deliberately no filter on the credit note document type: related_invoice_id is
#[ExternalReadonly] and only buildCreditNote() of both generators writes it. A type filter
would silently drop a document from the ledger after a seed change, and undercounting lets a
line be credited twice. Counting extra is safe (it only rejects more).
Parameters
- $sourceInvoiceId : int
Return values
array<int, Invoice>invoicedQuantities()
Returns the invoiced quantity per source line.
private
invoicedQuantities(iterable<string|int, mixed> $lines) : array<int, TLedgerLine>
Parameters
- $lines : iterable<string|int, mixed>
Tags
Return values
array<int, TLedgerLine>invoicedVoucherAmounts()
Returns the fixed discount amount per source voucher.
private
invoicedVoucherAmounts(iterable<string|int, mixed> $vouchers) : array<int, TLedgerVoucher>
The amount is positive on the document (the calculator subtracts it). A purely percentage
voucher has price = null, so its fixed part is 0; its proportion comes from copying percents.
Parameters
- $vouchers : iterable<string|int, mixed>
Return values
array<int, TLedgerVoucher>money()
private
money(string|null $value) : numeric-string
Parameters
- $value : string|null
Return values
numeric-stringrelatedLineId()
private
relatedLineId(InvoiceItem|InvoiceShipper|InvoicePaymentMethod $line) : int|null
Parameters
Return values
int|nullwholeQuantity()
Returns the line quantity as whole pieces and fails loudly otherwise.
private
wholeQuantity(InvoiceItem|InvoiceShipper|InvoicePaymentMethod $line, string $origin) : int
A silent (int) cut would give a cap of 2 for 2.5 (the remaining 0.5 locked forever) and drop a
0.5 line from the ledger. Until the ledger supports decimals, throwing is the only safe reaction;
a credit note for such an invoice is manual work.
Parameters
- $line : InvoiceItem|InvoiceShipper|InvoicePaymentMethod
- $origin : string
Tags
Return values
intwithRemainders()
Computes `remaining`, clamped to 0 so an overdrawn line (corrupt data) cannot lend a negative balance; the cap just closes.
private
withRemainders(TCreditNoteLedger $ledger) : TCreditNoteLedger
Parameters
- $ledger : TCreditNoteLedger