Powiadomienia i integracja zewnętrzna¶
Każda zmiana stanu paczki emituje event do RabbitMQ. NotificationWorker subscrybuje wszystkie shipment.* i obecnie loguje do AuditLog z polem wouldSend: 'email_via_smtp_in_etap7' — faktyczne SMTP i webhook do external system są planowane na etap 7.
Pipeline eventów¶
flowchart LR
A[ShipmentsService<br/>state change] -->|publish| B[(RabbitMQ<br/>exchange: smartbox.events)]
B -->|routing key shipment.#| C[NotificationWorker]
B -->|routing key machine.#| D[OpsAlertWorker<br/>placeholder]
C -->|AuditLog INSERT<br/>wouldSend=email...| E[(Postgres)]
C -.->|📌 ETAP 7| F[SMTP server]
C -.->|📌 ETAP 7| G[External webhook<br/>POST configured URL]
F -.-> H[Recipient inbox]
G -.-> I[Zewnętrzny system zamówień<br/>oznacza order jako<br/>do-odbioru / zrealizowane]
Eventy emitowane¶
Wszystkie zdefiniowane w backend/src/infra/amqp/event-types.ts:
| Routing key | Trigger | Payload | Subscriber action (obecny) | Subscriber action (etap 7) |
|---|---|---|---|---|
shipment.created |
POST /shipments |
{ shipmentId, trackingId, recipientId, createdById } |
Audit log placeholder | Email do recipient: "Stworzono przesyłkę {trackingId}" |
shipment.assigned |
deposit/begin-existing lub admin assign |
{ shipmentId, machineId, lockerId, emittedAt } |
Audit log | Email do recipient: "Skrytka {N} przyjmie Twoją paczkę" |
shipment.deposited |
POST /shipments/:id/deposited |
{ shipmentId, machineId, lockerId, emittedAt } |
Audit log | Email do recipient + webhook external "do odbioru" |
shipment.picked_up |
POST /pickup/:id/complete |
{ shipmentId, machineId, lockerId, pickedUpAt } |
Audit log | Email do nadawcy + webhook external "zrealizowane" ✓ |
shipment.cancelled |
admin cancel | { shipmentId, cancelledAt, reason } |
Audit log | Email do obu + webhook external "anulowane" |
shipment.expired |
cron >expiresAt (TODO) |
{ shipmentId, expiredAt, lastLocker } |
Audit log | Email do recipient + ops + webhook |
machine.heartbeat |
każde 30s z kiosku | { machineId, status, lastSeenAt } |
(drop — tylko keep-alive) | — |
machine.offline |
MachineWorker cron flip ONLINE→OFFLINE | { machineId, code, lastSeenAt, thresholdSec } |
Audit log placeholder | Email do ops "Maszyna {code} offline" |
machine.task.open_locker |
admin emergency open | { machineId, lockerId, taskId } |
dispatched do kiosku przez task poll | — |
Sequence: shipment.deposited → recipient + external¶
Stan obecny (etap 5.1):
sequenceDiagram
participant SS as ShipmentsService.markDeposited
participant MQ as RabbitMQ
participant NW as NotificationWorker
participant DB as Postgres AuditLog
SS->>MQ: publish 'shipment.deposited'
MQ->>NW: deliver
NW->>DB: INSERT AuditLog<br/>{ action: 'system.notification_shipment.deposited',<br/>payload: { wouldSend: 'email_via_smtp_in_etap7',<br/>routingKey, shipmentId, machineId, lockerId } }
Note over NW: ⛔ KONIEC — nic więcej się nie dzieje<br/>(placeholder dla etap 7)
Stan po etap 7 (plan):
sequenceDiagram
participant SS as ShipmentsService.markDeposited
participant MQ as RabbitMQ
participant NW as NotificationWorker
participant SMTP as SMTP server
participant EXT as External system<br/>(np. ErgoFlow webhook URL)
SS->>MQ: publish 'shipment.deposited'
MQ->>NW: deliver
par Email do recipient
NW->>SMTP: SEND mail<br/>To: recipient@firma.pl<br/>Subject: "Twoja paczka czeka"<br/>Body: "skrytka {N}, kod: {trackingId}"
and Webhook do external
NW->>EXT: POST {externalWebhookUrl}<br/>{ event: 'shipment.deposited',<br/>shipmentId, trackingId, machineCode,<br/>lockerNumber, depositedAt }
EXT->>EXT: oznacza order jako<br/>"do odbioru u nas"
EXT-->>NW: 200 OK (lub retry 3x na fail)
end
NW->>DB: INSERT AuditLog (zachowane dla audytu)
Konfiguracja webhook (planowana — etap 7)¶
Każdy shipment może mieć opcjonalny webhookUrl przekazany przy create:
json
POST /shipments
{
"recipientId": "...",
"webhookUrl": "https://erp.firma.pl/api/orders/123/smartbox-event",
"webhookSecret": "shared-secret-for-hmac"
}
Backend będzie POSTował do tego URL na każdy event paczki z HMAC-SHA256 signature w nagłówku X-SmartBox-Signature. External potwierdza 200 OK, na 4xx/5xx kolejka retry z exponential backoff (3 próby, potem dead-letter).
Alternatywnie webhook na poziomie maszyny lub globalnej konfiguracji (jeden endpoint dla wszystkich paczek od tego klienta).
Auto-fault reports (już działa)¶
Niezależnie od shipment events, kiosk wysyła fault reports do panelu:
sequenceDiagram
participant K as Kiosk
participant B as Backend
participant P as Panel<br/>(System → Zgłoszenia)
Note over K: USER fault: operator klika "Zgłoś obsłudze"<br/>LUB MACHINE fault: master E07 auto-detected
K->>B: POST /machines/me/fault-reports<br/>{ reportedBy: USER|MACHINE,<br/>currentPhase, faultCode,<br/>sensorByte, masterMW100,<br/>timeline: [...] }
B->>B: INSERT KioskFaultReport
Note over P: admin/courier widzi natychmiast<br/>w /dashboard/system/fault-reports
Typy fault codes:
- E07 — master timeout (drzwi nie otworzyły się / nie zamknęły)
- E07-DRIVER — driver-side timeout (brak ACK z mastera)
- E16 — generyczny master fault
- HID_BRIDGE_DOWN — RFID bridge stopped responding (Flutter BridgeHealthMonitor)
- (puste) — USER report bez specyficznego kodu
Webhook bridge health (już działa)¶
BridgeHealthMonitor (v1.1.19) na froncie Fluttera monitoruje rfid-bridge.log. Jeśli auto-restart 3× nie pomógł:
sequenceDiagram
participant FH as Flutter<br/>BridgeHealthMonitor
participant K as Kiosk
participant B as Backend
Note over FH: rfid-bridge.log nie advancuje >60s<br/>auto-restart attempt 1/3, 2/3, 3/3<br/>wszystkie fail
FH->>B: POST /machines/me/fault-reports<br/>{ reportedBy: MACHINE,<br/>faultCode: 'HID_BRIDGE_DOWN',<br/>currentPhase: 'login',<br/>reporterMessage: "...",<br/>timeline: [{ kind: 'bridge.health.failed', ... }] }
Note over K: UI: czerwony banner<br/>"Czytnik kart nie odpowiada"
Maszyna offline¶
sequenceDiagram
participant MW as MachineWorker<br/>(cron 1min)
participant DB as Postgres
participant MQ as RabbitMQ
Note over MW: SELECT machines WHERE status=ONLINE<br/>AND lastSeenAt < NOW - 2min
MW->>DB: UPDATE status=OFFLINE
MW->>DB: INSERT AuditLog system.machine_offline
MW->>MQ: publish 'machine.offline'<br/>{ machineId, code, lastSeenAt }
Note over MQ: 📌 etap 7: email do ops
W panelu maszyna ma status badge OFFLINE (czerwony). Cron flipuje automatycznie z powrotem na ONLINE jak tylko heartbeat wróci.
Audit log — kompletna historia¶
Każdy event + każda akcja użytkownika w panelu/kiosku jest logowana w AuditLog:
| actorType | Kto | Przykłady action |
|---|---|---|
| USER | operator/admin z panelu | shipment.create, shipment.assign_locker, locker.emergency_open |
| MACHINE | kiosk via API key | shipment.deposit_started (TODO), pickup.completed |
| SYSTEM | worker / cron | system.notification_shipment.*, system.machine_offline, system.ota_auto_cancelled |
Panel: /dashboard/system/audit z filtrami (actorType, action, entityType, range dat).
Podsumowanie obecny vs etap 7¶
| Co | Stan obecny (etap 5.1) | Etap 7 (plan) |
|---|---|---|
| Email do recipient po DEPOSITED | ❌ tylko audit placeholder | ✅ SMTP |
| Email do nadawcy po PICKED_UP | ❌ | ✅ SMTP |
| Webhook external po DEPOSITED | ❌ | ✅ POST z HMAC sign |
| Webhook external po PICKED_UP (status: zrealizowane) | ❌ | ✅ POST |
| Webhook external po CANCELLED / EXPIRED | ❌ | ✅ POST |
| Fault reports w panelu | ✅ działa | |
| Auto-fault HID_BRIDGE_DOWN | ✅ działa (v1.1.19) | |
| MachineWorker auto-offline + audit | ✅ działa | |
| Email ops na machine.offline | ❌ tylko audit placeholder | ✅ SMTP |
| Expiration cron + email | ❌ brak cron'a | ✅ |
→ Dalej: Edge cases