Ładowanie paczki (deposit) — wszystkie warianty¶
Kurier z paczką w ręku idzie do kiosku, loguje się, wpisuje kod nadania (ostatnie 6 cyfr trackingId), kiosk rezerwuje skrytkę, otwiera ją, kurier wkłada paczkę i zamyka drzwi.
Sekwencja happy-path¶
sequenceDiagram
autonumber
actor C as Kurier
participant K as Kiosk (Flutter)
participant B as Backend API
participant DB as Postgres
participant H as Hardware<br/>(Modbus master)
participant MQ as RabbitMQ
C->>K: PIN 261438 (lub RFID swipe)
K->>B: POST /deposit/verify-sender
B->>DB: SELECT credential
B-->>K: 200 { user: {...} }
K->>K: pokaż menu główne
C->>K: tap "Nadaj paczkę" → "Kod nadania"
C->>K: wpisz "995354"
K->>B: GET /deposit/lookup?last6=995354
B->>DB: SELECT shipment WHERE trackingId LIKE '%995354'
B-->>K: 200 { shipmentId, recipientName }
K->>K: pokaż "Wybierz rozmiar dla {recipientName}"
C->>K: wybiera M → "Otwórz teraz"
K->>B: POST /deposit/begin-existing { shipmentId, size: 'M' }
B->>DB: BEGIN TRANSACTION
B->>DB: SELECT FIRST locker WHERE FREE+PARCEL+M FOR UPDATE
B->>DB: UPDATE locker SET status=RESERVED
B->>DB: UPDATE shipment SET machineId, lockerId,<br/>status=ASSIGNED, assignedAt
B->>MQ: publish shipment.assigned
B->>DB: COMMIT
B-->>K: 200 { lockerNumber: 5 }
K->>K: push LockerOpenScreen, phase=opening
K->>H: hardware.isClosed(5) — pre-flight
H-->>K: true (zamknięte)
K->>H: hardware.open(5) — Modbus MW100 pulse
H-->>K: opened event
K->>K: phase=open<br/>UI: "Skrytka otwarta, włóż paczkę i zamknij"
C->>H: fizycznie wkłada paczkę
C->>H: zamyka drzwi
H-->>K: closed event
K->>K: phase=open → _onCloseDetected (phase MUSI być open)<br/>phase=closed
K->>B: POST /shipments/:id/deposited
B->>DB: UPDATE locker SET status=OCCUPIED
B->>DB: UPDATE shipment SET status=DEPOSITED,<br/>depositedAt=NOW
B->>MQ: publish shipment.deposited
B-->>K: 200
K->>K: phase=done → reopen panel<br/>[Otwórz ponownie] [Zakończ] [Anuluj-paczki-nie-ma]
C->>K: tap "Zakończ"
K->>K: pop → 🟢 DepositSuccessScreen "Paczka nadana"<br/>z QR-kodem
State machine deposit (z perspektywy backendu)¶
stateDiagram-v2
[*] --> CREATED: stworzone przez API/panel
CREATED --> ASSIGNED: deposit/begin-existing<br/>(locker RESERVED)
ASSIGNED --> DEPOSITED: shipments/:id/deposited<br/>(locker OCCUPIED, depositedAt set)
ASSIGNED --> CREATED: deposit/abandon-locker<br/>(locker BROKEN, reset)
DEPOSITED --> CREATED: deposit/cancel-after-deposit<br/>(locker FREE!, full rollback)
DEPOSITED --> [*]: dalej do pickup
CANCELLED --> [*]
note right of CREATED
Kurier może wpisać kod, znów rezerwuje locker
end note
Wariant 1V1 — Happy path¶
Patrz sekwencja powyżej. Wynik:
| Layer | Stan |
|---|---|
| Locker | OCCUPIED |
| Shipment | DEPOSITED + lockerId + depositedAt=NOW |
| Panel timeline | ✅ Utworzono ✅ Przypisano ✅ Złożono w skrytce |
| Kiosk UI | 🟢 DepositSuccessScreen z QR-kodem |
| Powiadomienia | Audit notification_shipment.deposited (placeholder etap 7: email do recipient + webhook external) |
Wariant 1V2 — drzwi otwarte, kurier nie zamyka¶
Kurier otworzył skrytkę ale trzyma drzwi otwarte ponad 20 sekund (zapomniał, zacięły się, zniknął).
sequenceDiagram
autonumber
actor C as Kurier
participant K as Kiosk
participant B as Backend
Note over K: phase=open, czeka na 'closed'
Note over C: ⏱ trzyma drzwi 20+ sekund
K->>K: _openPhaseDeadline timer fires<br/>phase=userConfirmClosed<br/>Dialog "Czy skrytka zamknięta?"
alt Operator: "Zgłoś obsłudze"
C->>K: tap Zgłoś obsłudze
K->>B: POST /machines/me/fault-reports<br/>{ reportedBy: USER, currentPhase: userConfirmClosed }
K->>K: phase=faultReported<br/>_phasePriorToFault=userConfirmClosed
Note over K: v1.1.21: TYLKO [Anuluj]<br/>(brak Spróbuj ponownie!)<br/>Tytuł: "Drzwi nie zostały zamknięte"
C->>K: tap Anuluj
K->>K: pop 'fault'
K->>B: POST /deposit/abandon-locker
B->>B: locker RESERVED → BROKEN<br/>shipment ASSIGNED → CREATED<br/>(machineId, lockerId, assignedAt cleared)
K->>K: 🔴 DepositFailureScreen<br/>"Paczka nie została nadana"
end
Stan po Anuluj:
| Layer | Stan |
|---|---|
| Locker | BROKEN (operator wymaga sprawdzenia w panelu) |
| Shipment | CREATED + lockerId=NULL + depositedAt=NULL |
| Panel Zgłoszenia | USER-reported fault, currentPhase: userConfirmClosed, sensorByte + masterMW100 |
| Kiosk UI | 🔴 DepositFailureScreen |
Wariant 1V3 — drzwi się nie otwierają (master E07)¶
Mechaniczna blokada zamka albo kurier siłą trzymał drzwi zamknięte podczas pulse.
sequenceDiagram
autonumber
participant K as Kiosk
participant H as Hardware
K->>H: hardware.open(N) — Modbus pulse
H-->>K: E07 fault event<br/>(door open timeout)
K->>K: _autoReportMachineFault('E07')<br/>phase=faultReported<br/>_phasePriorToFault=opening
Note over K: 🟡 Fault card:<br/>Tytuł: "Wystąpił problem techniczny"<br/>[🔄 Spróbuj ponownie] [Anuluj]
alt Spróbuj ponownie
K->>K: _retryFromFault → forceCommand=true<br/>phase=opening znów
K->>H: hardware.open(N) — re-pulse
H-->>K: opened (jeśli OK)<br/>LUB znów E07
end
alt Anuluj
K->>K: pop 'fault'
Note over K: → /deposit/abandon-locker<br/>locker BROKEN, shipment CREATED
end
Stan po Anuluj: identyczny jak 1V2 ale fault report MACHINE (auto-detected) z faultCode: E07.
Wariant 1V4 — driver timeout (brak ACK z mastera)¶
sequenceDiagram
participant K as Kiosk
participant H as Hardware driver
K->>H: hardware.open(N)
Note over H: czeka 12s na ACK z mastera
H-->>K: throw TimeoutException
K->>K: catch w _runOpen → _autoReportMachineFault('E07-DRIVER')<br/>phase=faultReported
Identyczne UI jak 1V3, fault code w report = E07-DRIVER. Diagnostyka: master prawdopodobnie nie odpowiada na cały bus.
Wariant 1V5 — drzwi już otwarte przed pulse¶
Sytuacja: poprzednia sesja crashed mid-deposit i zostawiła drzwi otwarte. Kurier teraz próbuje na tę samą skrytkę.
sequenceDiagram
participant DL as deposit_lookup_screen
participant K as LockerOpenScreen
participant H as Hardware
DL->>H: hardware.isClosed(N) — pre-flight
H-->>DL: false (drzwi otwarte!)
DL->>DL: POST /deposit/abandon-locker<br/>(locker BROKEN, shipment CREATED)
DL->>DL: 🔴 DepositFailureScreen<br/>"Wybrana skrytka nie spełnia warunków"
Note over DL,H: NIE pushuje LockerOpenScreen
Albo wewnątrz LockerOpenScreen (jeśli pre-flight nie wykrył):
sequenceDiagram
participant K as LockerOpenScreen
participant H as Hardware
K->>H: isClosed(N) wewnątrz _runOpen
H-->>K: false
K->>K: SKIP open pulse<br/>phase=open od razu<br/>(zakłada że ktoś już otworzył)
Note over K: Czeka na fizyczne zamknięcie drzwi
Wariant 1V6 — "Paczki nie ma w skrytce" po sukcesie¶
Operator pomyślnie zamknął drzwi (sukces, status DEPOSITED), ale uświadomił sobie że zapomniał włożyć paczkę, lub paczka była uszkodzona i ją wyciągnął.
sequenceDiagram
actor C as Kurier
participant K as Kiosk
participant B as Backend
Note over K: po sukcesie, reopen panel widoczny<br/>[Otwórz ponownie] [Zakończ] [🔴 Anuluj-paczki-nie-ma]
C->>K: tap [🔴 Anuluj — paczki nie ma w skrytce]
K->>B: POST /deposit/cancel-after-deposit { shipmentId }
B->>B: walidacja: status MUST be DEPOSITED, inaczej 409
B->>B: UPDATE locker SET status=FREE (z OCCUPIED!)<br/>UPDATE shipment SET status=CREATED,<br/>depositedAt=NULL, lockerId=NULL,<br/>machineId=NULL, assignedAt=NULL
B-->>K: 200 { lockerFreed: true }
K->>K: pop 'cancel-after-deposit'<br/>🔴 DepositFailureScreen
Stan po 1V6:
| Layer | Stan |
|---|---|
| Locker | FREE (nie BROKEN — operator potwierdził że pusty) |
| Shipment | CREATED (znów dostępna do nadania) |
| Kiosk UI | 🔴 DepositFailureScreen |
Różnica od 1V2 (Anuluj po fault)
Po fault locker idzie do BROKEN — bo nie wiemy czy zacięł się, więc lepiej żeby admin sprawdził.
Po Anuluj-paczki-nie-ma locker idzie do FREE — bo operator EXPLICITLY powiedział że jest pusty.
Wariant 1V7 — "Otwórz ponownie" na reopen panel¶
sequenceDiagram
actor C as Kurier
participant K as Kiosk
participant H as Hardware
Note over K: phase=done, reopen panel
C->>K: tap [Otwórz ponownie]
K->>K: _reopen() guard: phase MUST be done<br/>(v1.1.14 — drop racy double-taps)
K->>K: phase=opening
K->>H: hardware.open(N) — re-pulse
H-->>K: opened
K->>K: phase=open<br/>"Skrytka otwarta — domknij gdy gotowe"
C->>H: zamyka drzwi znów
H-->>K: closed
K->>B: POST /shipments/:id/deposited (już DEPOSITED!)
B-->>K: 409 ConflictException
K->>K: catch silent, phase=done<br/>reopen panel znów
Backend już ma DEPOSITED → drugi markDeposited rzuca 409. Kiosk catch'uje silently. Stan paczki bez zmian. Operator może kliknąć [Otwórz ponownie] dowolną ilość razy bez psucia stanu.
Wariant 1V8 — inactivity podczas flow¶
flowchart LR
A[Operator na ekranie menu] -- 30s bez tap --> B[Inactivity overlay]
B -- 10s countdown --> C[Auto pop_until login]
C --> D{Gdzie był w flow?}
D -- pre lookup --> E[shipment dalej CREATED]
D -- post begin-existing, pre open --> F[shipment ASSIGNED + locker RESERVED]
D -- w LockerOpenScreen --> G[hold token blokuje inactivity tu]
F -.->|⚠ TODO cleanup cron| F
Open: cleanup orphan ASSIGNED
Jeśli operator porzucił session po begin-existing ale przed otwarciem drzwi, locker zostaje RESERVED + shipment ASSIGNED. Brak auto-cleanup cron'a — admin musi ręcznie zwolnić w panelu (lub kurier próbuje znów: backend wykrywa konflikt, ale obecnie tylko 409).
Wariant 1V9 — sieć padnie podczas lookup¶
Kiosk pokazuje "Brak połączenia, spróbuj ponownie". Stan: bez zmian (lookup to GET, idempotent). Operator próbuje za chwilę.
Wariant 1V10 — sieć padnie po pulse, przed markDeposited¶
sequenceDiagram
participant K as Kiosk
participant SR as SyncService outbox
participant B as Backend
Note over K: drzwi zamknięte, onClosed() leci
K->>B: POST /shipments/:id/deposited
Note over K: ❌ network timeout
K->>SR: queue event 'deposit.completed'<br/>{ shipmentId, depositedAt: NOW }
Note over K: UI dalej leci jakby OK<br/>(cache: shipment status DEPOSITED locally)
Note over B,SR: ... sieć wraca po 5 min ...
SR->>B: POST /machines/me/replay [events]
B->>B: idempotent markDeposited<br/>(WHERE status='ASSIGNED', no-op jeśli już DEPOSITED)
B-->>SR: 200 OK
Note over K,SR: panel widzi DEPOSITED z opóźnieniem
Macierz deposit-fault → stan końcowy¶
| Wariant | Trigger | Phase Prior To Fault | Buttons | Pop result | Locker | Shipment | Fault report |
|---|---|---|---|---|---|---|---|
| 1V1 | happy | — | Zakończ/Otwórz pon. | 'success' | OCCUPIED | DEPOSITED | — |
| 1V2 | drzwi otwarte, Zgłoś obsłudze | userConfirmClosed | Tylko Anuluj | 'fault' | BROKEN | CREATED | USER |
| 1V3 | master E07 (drzwi mech) | opening | Spróbuj ponownie + Anuluj | 'fault' | BROKEN | CREATED | MACHINE |
| 1V4 | driver timeout | opening | Spróbuj ponownie + Anuluj | 'fault' | BROKEN | CREATED | MACHINE |
| 1V5 | pre-flight: drzwi otwarte | — | — | — | BROKEN (przez abandon) | CREATED | — |
| 1V6 | "Paczki nie ma w skrytce" | — | — | 'cancel-after-deposit' | FREE | CREATED | — |
| 1V7 | Otwórz ponownie | — | — | — | OCCUPIED | DEPOSITED | — |
| 1V8 | inactivity poza screen | — | — | — | zależy | zależy | — |
| 1V9 | sieć padnie przy lookup | — | — | — | bez zmian | CREATED | — |
| 1V10 | sieć padnie przy markDeposited | — | — | — | OCCUPIED (z opóźnieniem) | DEPOSITED (z opóźnieniem) | — |
→ Dalej: Odbiór (pickup)