Sklad#
Modul Store eviduje množství — sám nic neprodává. E-shop mu říká „rezervuj“,
„potvrď“, „uvolni“; sklad na to odpovídá a vede o tom doklady. Kód:
app/UI/Api/Store/.
Z čeho se modul skládá#
| Entita | Role |
|---|---|
StockItem |
skladová karta — vazba na prodejní sadu e-shopu |
StockLevel |
množství na jednom skladu: quantity a reserved |
StockBatch |
šarže — číslo, expirace, nákupní cena |
StockDocument |
doklad (příjemka, výdejka, převodka, inventura) |
StockMovement + StockMovementBatch |
pohyby a jejich rozpad na šarže |
StockReservation |
rezervace |
Warehouse, Supplier, Vacation |
číselníky a dovolené skladů |
| Služba | K čemu |
|---|---|
StockReader |
čtecí model dostupnosti a termínů dodání, bez zámků |
StockRecalculator |
noční přepočet a self-heal |
ReservationExpirer |
uvolňování prošlých rezervací |
MarginReporter |
marže z nákupních a prodejních cen |
Tři množství, prodává se podle dostupného#
reserved je DENORMALIZOVANÉ — drží se uložené, nedopočítává se
Selhání uprostřed operace může nechat rezervaci bez objednávky. Fyzický stav pak sedí, dostupný ne — zboží je na skladě, ale nejde ho prodat. Nic to nehlásí; najde se to jako „proč to píše vyprodáno“. Proto ten noční přepočet.
Čtení dostupnosti je konstantní počet dotazů
StockReader agreguje přes sklady, karty, dodavatele i dovolené nezávisle na
počtu položek, takže výpis celé kategorie e-shopu se dá zjistit levně. Kdo si
dostupnost počítá po položkách ve smyčce, vyrobí N+1.
Rozhraní pro e-shop: čtyři operace#
Sklad nabízí navenek čtyři metody a všechny berou SourceRef — dvojici
„který modul + jaký typ + které id“ (například objednávka č. 412 z e-shopu):
| Metoda | Co udělá |
|---|---|
reserve($source, $lines, $allowPartial, $expiresAt) |
drží množství pro daný zdroj |
confirm($source, $salePrices, $userId) |
promění rezervaci ve výdejku |
release($source) |
rezervaci uvolní |
post($documentId) |
potvrdí doklad a teprve tím pohne množstvím |
SourceRef slouží zároveň jako klíč idempotence i jméno zámku
(cms_store_src_<modul>_<typ>_<id>).
Opakované volání se stejným SourceRef je no-op, ne druhá rezervace
Právě proto se dá přechod stavu objednávky bezpečně zopakovat po pádu. Kdo si vymyslí vlastní klíč, tuhle vlastnost ztratí.
Pořadí zámků je vždy eshop → store
E-shop drží per-order zámek, sklad k němu přidá svůj nad zdrojem. Opačné pořadí by při souběhu vyrobilo uváznutí.
Postup: napojuji nový modul na sklad#
- Zaveď
SourceRefpro svůj typ dokladu — modul, typ, id. Musí být stabilní napříč opakováními téže operace. - Rezervuj při vzniku závazku (
reserve()), s platností odpovídající tomu, jak dlouho závazek žije. - Rozhodni, jestli povolíš částečnou rezervaci. E-shopový checkout ji nepovoluje
(
allowPartial = false) — zákazník nesmí zaplatit nekryté zboží. - Při dokončení volej
confirm(), při zrušenírelease(). Nikdy nesahej naquantitypřímo. - Prokliknij i cestu selhání: co se stane, když operace spadne mezi rezervací a potvrzením.
Přímý UPDATE množství obejde doklady i pohyby
Sklad pak sedí, ale nedá se dohledat proč. Každá změna množství musí projít dokladem — jinak inventura nemá s čím porovnávat.
Doklady#
Příjemka, výdejka, převodka, inventura. Dokud není doklad potvrzený (post()),
s množstvím nedělá nic — dá se rozpracovat, opravit i zahodit.
Potvrzený doklad se neopravuje ani nemaže
Vystaví se storno a pak nový doklad. Smazání by rozešlo aktuální stav s historií pohybů — a rozdíl se objeví až při inventuře, bez stopy, odkud vznikl.
Pohyby jsou snímky#
Pohyb ukládá název položky a zůstatek v okamžiku pohybu.
Přejmenování karty nezmění staré pohyby
Je to záměr — doklad má ukazovat, co na něm stálo tehdy. Stejná logika jako u faktury.
Šarže#
Odepisuje se od nejstarší. U zboží s expirací to zároveň odpovídá trvanlivosti.
Prošlá šarže se sama nevyřadí
Zůstane v evidenci a normálně se odepíše. Cron
/cron/store/batch/expiry-alert jen upozorní; vyřazení je ruční výdejka.
Rezervace a souběh#
Rezervace vzniká se závazkem a má platnost; po ní se uvolní cronem.
Platnost rezervace musí být sladěná s expirací objednávky
Uvolní-li se rezervace dřív, než vyprší objednávka, zboží se může prodat podruhé — a druhý zákazník zaplatí něco, co už není. Opačný nepoměr je jen neefektivita: zboží zbytečně dlouho blokované.
Cron uvolňování běží à 5 minut
/cron/store/reservation/expire. Delší interval znamená, že zboží zůstává
blokované po vypršení — a projeví se to jako „vyprodáno“, ne jako chyba.
Napojení na e-shop#
Skladové operace se vážou na prodejní sadu (ProductSet), tedy na tutéž entitu,
kterou e-shop prodává.
Hledání skladové operace na produktu nebo variantě nic nenajde
Vazba je až na sadě. Podrobně E-shop, oddíl o datovém modelu.
Naplánované úlohy#
| Endpoint | Co dělá | Doporučený interval |
|---|---|---|
/cron/store/reservation/expire |
uvolní prošlé rezervace | à 5 minut |
/cron/store/batch/expiry-alert |
upozorní na blížící se expiraci šarží | denně ráno |
/cron/store/stock/recount |
noční přepočet a self-heal denormalizovaných hodnot | denně v noci |
Bez nočního přepočtu se rozdíl v reserved nikdy nesrovná
Denormalizovaná hodnota se opraví jedině tímhle během. Nespuštěná úloha se neprojeví chybou — jen postupně přibývá zboží, které je na skladě a přesto se tváří jako nedostupné.
Kam sáhnout#
| Chci | Kde |
|---|---|
| rozhraní rezervací | app/UI/Api/Store/Models/Managers/StockReservationManager.php |
| potvrzování dokladů | app/UI/Api/Store/Models/Managers/StockDocumentManager.php |
| čtení dostupnosti a termínů | app/UI/Api/Store/Models/Services/StockReader.php |
| přepočet a self-heal | app/UI/Api/Store/Models/Services/StockRecalculator.php |
| klíč idempotence a zámku | app/UI/Api/Store/Models/DTO/SourceRef.php |
| cron úlohy | app/UI/Cron/Store/Presenters/ |
Navazující kapitoly: E-shop · Fakturace · Naplánované úlohy · Životní cyklus manageru