Przejdź do treści

Ł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)