CoolCMS API

StockRecalculator
in package

FinalYes

Nightly self-heal of stock levels and item prices, run by cron.

Uses raw DBAL with its own private copy of the bcmath/retry helpers (same pattern as DocumentService / SourceLockTransactionTrait); it is a self-contained service, not a shared trait.

Ground truth is the append-only journal: stock_movements of non-draft documents, i.e. posted and cancelled. A cancelled document's original movements are not deleted; their effect is offset by the storno counter-document's own movements, so both must be summed for the net physical effect to come out right. Draft movements are not on stock yet and are excluded. stock_levels.quantity is recomputed from that journal sum and corrected on drift. stock_levels.reserved is recomputed from active reservations and corrected on drift.

stock_batches are only ever a fresh insert per FEFO layer, never topped up in place, so there is no persisted "originally received" quantity to reconstruct a partially consumed row. An individual batch row therefore cannot be auto-corrected without risking a wrong FIFO layer (poisoning future COGS and average). The aggregate batch quantity per stock item and warehouse is only compared against GREATEST(corrected level.quantity, 0) (the invariant DocumentService asserts after every write); a mismatch is logged as an alarm for manual review, never patched.

recountItemPrices() needs no chronological replay either: average and last price are recomputed the same way DocumentService::recalcCard() and recalcLastPurchase() do, as the weighted average of the remaining priced batches and the latest posted, non-storno receipt.

Both methods sweep the whole table on every run, paginated in $batchSize-row pages (keyset: id > lastId ORDER BY id ASC LIMIT batchSize) so memory stays bounded. Capping the run at N rows would silently stop covering a catalog larger than N and defeat the purpose of a self-heal cron. Each row is fixed in its own short READ COMMITTED transaction (with deadlock retry), so the sweep never holds one giant lock across the table while live traffic keeps writing.

Table of Contents

Constants

BATCHES  : string = 'cms_mod_store_stock_batches'
DEFAULT_BATCH_SIZE  : int = 200
DEFAULT_STALE_DAYS  : int = 30
DOCUMENTS  : string = 'cms_mod_store_stock_documents'
ITEMS  : string = 'cms_mod_store_stock_items'
LEVELS  : string = 'cms_mod_store_stock_levels'
MAX_ATTEMPTS  : int = 3
MOVEMENTS  : string = 'cms_mod_store_stock_movements'
RESERVATIONS  : string = 'cms_mod_store_stock_reservations'
SCALE  : int = 3
SETTING_MODULE  : int = 10
SETTINGS  : string = 'cms_system_settings'

Properties

$em  : EntityManagerInterface
$settingContext  : SettingContext

Methods

__construct()  : mixed
recountItemPrices()  : array{checked: int, driftAvg: int, driftLast: int}
recountLevels()  : array{checked: int, driftQuantity: int, driftReserved: int, batchDrift: list, quantityAfterMismatch: list, missingLevels: list, staleReservations: list}
cmp()  : int
dec()  : numeric-string
findMissingLevels()  : void
Safety net: a stock item and warehouse combination with batches or journal entries but no `stock_levels` row at all means the row was deleted or never created outside the upsert path. That is structurally impossible, hence a strong alarm, not a quiet fix.
findStaleReservations()  : void
journalSum()  : numeric-string
n()  : numeric-string
recountOneItemPrice()  : void
recountOneLevel()  : void
rInt()  : int
rStr()  : string
rStrN()  : string|null
runInTransaction()  : T
sameNullableDec()  : bool
settingInt()  : int
settingValue()  : string|null
Same fallback rationale as {@see DocumentService::settingValue()} — CLI/harness contexts.

Constants

RESERVATIONS

private string RESERVATIONS = 'cms_mod_store_stock_reservations'

Properties

Methods

recountItemPrices()

public recountItemPrices([int|null $batchSize = null ]) : array{checked: int, driftAvg: int, driftLast: int}
Parameters
$batchSize : int|null = null
Return values
array{checked: int, driftAvg: int, driftLast: int}

recountLevels()

public recountLevels([int|null $batchSize = null ]) : array{checked: int, driftQuantity: int, driftReserved: int, batchDrift: list, quantityAfterMismatch: list, missingLevels: list, staleReservations: list}
Parameters
$batchSize : int|null = null
Return values
array{checked: int, driftQuantity: int, driftReserved: int, batchDrift: list, quantityAfterMismatch: list, missingLevels: list, staleReservations: list}

cmp()

private cmp(string $a, string $b) : int
Parameters
$a : string
$b : string
Return values
int

dec()

private dec(string $value) : numeric-string
Parameters
$value : string
Return values
numeric-string

findMissingLevels()

Safety net: a stock item and warehouse combination with batches or journal entries but no `stock_levels` row at all means the row was deleted or never created outside the upsert path. That is structurally impossible, hence a strong alarm, not a quiet fix.

private findMissingLevels(Connection $conn, array{checked: int, driftQuantity: int, driftReserved: int, batchDrift: list, quantityAfterMismatch: list, missingLevels: list, staleReservations: list} &$stats) : void
Parameters
$conn : Connection
$stats : array{checked: int, driftQuantity: int, driftReserved: int, batchDrift: list, quantityAfterMismatch: list, missingLevels: list, staleReservations: list}

findStaleReservations()

private findStaleReservations(Connection $conn, array{checked: int, driftQuantity: int, driftReserved: int, batchDrift: list, quantityAfterMismatch: list, missingLevels: list, staleReservations: list} &$stats) : void
Parameters
$conn : Connection
$stats : array{checked: int, driftQuantity: int, driftReserved: int, batchDrift: list, quantityAfterMismatch: list, missingLevels: list, staleReservations: list}

journalSum()

private journalSum(Connection $conn, int $itemId, int $warehouseId) : numeric-string
Parameters
$conn : Connection
$itemId : int
$warehouseId : int
Return values
numeric-string —

journal ground truth (sum of signed quantity of non-draft documents)

n()

private n(string $value) : numeric-string
Parameters
$value : string
Return values
numeric-string

recountOneItemPrice()

private recountOneItemPrice(Connection $conn, int $itemId, array{checked: int, driftAvg: int, driftLast: int} &$stats) : void
Parameters
$conn : Connection
$itemId : int
$stats : array{checked: int, driftAvg: int, driftLast: int}

recountOneLevel()

private recountOneLevel(Connection $conn, int $itemId, int $warehouseId, array{checked: int, driftQuantity: int, driftReserved: int, batchDrift: list, quantityAfterMismatch: list, missingLevels: list, staleReservations: list} &$stats) : void
Parameters
$conn : Connection
$itemId : int
$warehouseId : int
$stats : array{checked: int, driftQuantity: int, driftReserved: int, batchDrift: list, quantityAfterMismatch: list, missingLevels: list, staleReservations: list}

rInt()

private rInt(mixed $value) : int
Parameters
$value : mixed
Return values
int

rStr()

private rStr(mixed $value) : string
Parameters
$value : mixed
Return values
string

rStrN()

private rStrN(mixed $value) : string|null
Parameters
$value : mixed
Return values
string|null

runInTransaction()

private runInTransaction(callable(Connection): T $fn) : T
Parameters
$fn : callable(Connection): T
Tags
template
Return values
T

sameNullableDec()

private sameNullableDec(string|null $a, string|null $b) : bool
Parameters
$a : string|null
$b : string|null
Return values
bool

settingInt()

private settingInt(string $key, int $default) : int
Parameters
$key : string
$default : int
Return values
int

settingValue()

Same fallback rationale as {@see DocumentService::settingValue()} — CLI/harness contexts.

private settingValue(string $key) : string|null
Parameters
$key : string
Return values
string|null
On this page

Search results