Przejdź do treści

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.quantity jest cache'em = liczba egzemplarzy IN_STOCK.

Wydawanie (kiosk)

  1. Na karcie produktu w siatce: stan „W maszynie: S szt." + stepper „− N +" obok „Wydaj" (etap-25). N startuje od domyślnej ilości produktu (Item.defaultDispenseQty, np. rękawice = 2), zakres 1..min(S, limit).
  2. Klik „Wydaj" → od razu wydanie z wybraną ilością (bez osobnego dialogu).
  3. Pętla: otwiera się skrytka z prowadzącym egzemplarzem wg strategii, user bierze offer = min(N, stan tej skrytki).
  4. Jeśli wziął mniej niż N → „Wziąłeś X z N — kolejna skrytka? [Koniec / Kolejna]".
  5. 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 tracksExpiry per 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, zakres 0..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 niesie expectedQty/foundQty per 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 chokepointInventoryService.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).

Powiązane