Etap 23–24 — Wiele sztuk w skrytce: ilości, strategie, ważność, egzemplarze¶
W skrócie
Odejście od reguły „1 skrytka = 1 sztuka". Skrytka trzyma N sztuk (pojemność per skrytka), wydanie bierze 1..N z wielu skrytek, a stan jest śledzony per egzemplarz (StockUnit) z kolejnością wg strategii maszyny (FIFO/LIFO/FEFO/RANDOM) i terminem ważności per produkt.
Pełny dokument projektowy (model, decyzje, ryzyka, plan): design-wieloilosc-skrytek.md w repo.
Jak to działa¶
Pojemność i stan¶
- Każda skrytka DISPENSE ma pojemność (max sztuk) ustawianą per skrytka w panelu (LockerBox i DynaBox).
- Stan to zbiór egzemplarzy (
StockUnit, 1 rekord = 1 sztuka):uid(unikalny id/tag — na razie sama ewidencja),lotCode,expiresAt,receivedAt.StockSlot.quantityjest cache'em = liczba egzemplarzyIN_STOCK.
Wydawanie (kiosk)¶
- Na karcie produktu w siatce: stan „W maszynie: S szt." + stepper „− N +" obok „Wydaj" (etap-25).
Nstartuje od domyślnej ilości produktu (Item.defaultDispenseQty, np. rękawice = 2), zakres1..min(S, limit). - Klik „Wydaj" → od razu wydanie z wybraną ilością (bez osobnego dialogu).
- Pętla: otwiera się skrytka z prowadzącym egzemplarzem wg strategii, user bierze
offer = min(N, stan tej skrytki). - Jeśli wziął mniej niż N → „Wziąłeś X z N — kolejna skrytka? [Koniec / Kolejna]".
- Brak kolejnych skrytek → „Wydano X z N". Każda skrytka rozliczana osobno (nieudane otwarcie nie traci stanu ani limitu). Limit usera zlicza faktycznie pobrane.
Ilość wydania — per PRODUKT (etap-25)¶
Ile sztuk schodzi na jedno wydanie to cecha produktu (nie skrytki): Item.defaultDispenseQty + dispenseQtyLocked. Ustawiane w kartotece (sekcja „Wydawanie").
dispenseQtyLocked |
Zachowanie na kiosku (krok steppera) |
|---|---|
| false (można pojedynczo) | krok 1: stepper −/+, domyślnie defaultDispenseQty, user reguluje 1..min(stan, limit) |
| true (zablokowana) | krok = defaultDispenseQty: można pobrać tylko wielokrotność paczki (np. 2 → 2, 4, 6, 8…) do min(stan, limit), zaokrąglone w dół do pełnej paczki |
Przykład: rękawice = 2. false → domyślnie 2, ale można 1, 2, 3… true → tylko 2, 4, 6, 8… (paczki po 2); jeśli na stanie/limicie nie ma pełnej paczki, „Wydaj" jest zablokowane. W obu trybach pętla wieloskrytkowa dobiera całość z kolejnych skrytek. Pojemność i stan zostają per skrytka (capacity/quantity).
Strategia wydania (per maszyna)¶
Ustawiana per maszyna (Machine.dispenseStrategy):
| Strategia | Kolejność schodzenia |
|---|---|
| FIFO | najstarsze przyjęcie pierwsze (receivedAt rosnąco) |
| LIFO | najnowsze przyjęcie pierwsze |
| FEFO | najszybciej tracące ważność pierwsze (expiresAt rosnąco) |
| RANDOM | losowo |
Termin ważności (per produkt)¶
- Flaga
tracksExpiryper produkt +expiredAction: BLOCK (nie wydawaj przeterminowanych) lub WARN (wydaj, ostrzegaj/raportuj). - FEFO ma sens dla produktów śledzących ważność.
Uzupełnianie (kiosk)¶
- Lista produktów do uzupełnienia: Powinno być / Jest / Brakuje; skrytka jest „do uzupełnienia", gdy
quantity < capacity(już nie tylko gdy pusta). - Po otwarciu skrytki: stepper „ile dołożono" (domyślnie = wolne miejsce
capacity − quantity, zakres0..wolne) + „Zatwierdź (N szt.)". - Commit RESTOCK odłożony do zatwierdzenia/okna domknięcia → operator ustala liczbę zanim stan się zapisze. Backend dopełnia do realnej pojemności skrytki.
Inwentaryzacja (kiosk) — ekran przeprojektowany (etap-26)¶
- Layout (wymóg klienta — zero scrolla): lewy panel = zdjęcie + nazwa produktu + podsumowanie na żywo (Powinno / Stwierdzono / Różnica); prawa tabela WSZYSTKICH skrytek kolumny na jednym ekranie. Wiersze stałej wysokości, stała kolejność „Wiersz X (#N)" (u góry 1, na dole N). Na LockerBoxie z >15 skrytkami w kolumnie → scroll po prawej; na DynaBoxie zawsze komplet bez scrolla.
- Po otwarciu skrytki: stepper −/+ (domyślnie = ilość wg systemu, zakres
0..pojemność) + „Zatwierdź (N szt.)" zamiast Pełna/Pusta.+/−pojawiają się tylko po otwarciu; inaczej wiersz pokazuje status (zwolniona / w kolejce / zacięcie / zinwentaryzowano). Zamknięcie skrytki akceptuje stan. - Podsumowanie liczy sztuki:
było → stwierdzono → różnica; dokument inwentaryzacji niesieexpectedQty/foundQtyper skrytka, różnica koryguje stan. - Skrytka, której nie udało się otworzyć (zacięcie F07), zostaje niezweryfikowana — stan bez zmian.
Architektura (dlaczego bezpiecznie)¶
Cały stan zmienia się przez jeden chokepoint — InventoryService.createMovement. Utrzymuje on cache quantity oraz egzemplarze StockUnit spójnie (OUT → oznacz DISPENSED/REMOVED wg strategii; IN → twórz IN_STOCK). Dzięki temu wydawanie/uzupełnianie/inwentaryzacja/alokacja są spójne bez przepisywania każdej z osobna. Movement zostaje trwałym śladem; egzemplarze DISPENSED są prune'owane cronem (retencja) — stąd baza się nie zapycha.
Status wdrożenia¶
| Chunk | Zakres | Status |
|---|---|---|
| 24.1 | Model: StockUnit, DispenseStrategy, Item.tracksExpiry/expiredAction; migracja + backfill |
live |
| 24.2 | Backend: silnik strategii w createMovement, confirm(quantity), issue offer/remaining, catalog realny stan |
live |
| 24.3 | Kiosk (1.5.55): picker ilości + pętla wieloskrytkowa „kolejna skrytka?" | live (rollout) |
| 24.4 | Uzupełnianie: ilość per skrytka (stepper, domyślnie wolne miejsce) + backend do realnej pojemności (kiosk 1.5.56) | live (rollout) |
| 24.5 | Inwentaryzacja wierszami: stepper −/+ liczbowo, suma sztuk, expectedQty/foundQty (kiosk 1.5.56) |
live (rollout) |
| 24.6 | Web: capacity per skrytka + occupancy realny stan, strategia per maszyna, ważność per produkt (flaga + BLOCK/WARN) |
live |
| 24.7 | Retencja (cron prune DISPENSED), podgląd egzemplarzy, raport „wkrótce przeterminowane" + E2E |
do zrobienia |
Migracja bezbolesna
Istniejące skrytki LockerBox zostają capacity = 1 dopóki operator nie podniesie w panelu (24.6) — zero nagłej zmiany zachowania. Backfill stworzył egzemplarze 1:1 z obecnych ilości (zweryfikowane: liczba egzemplarzy == suma quantity).