Hệ thống thông báo WELIFE
Domain: D-NOTIF · WP: WP-NOTIF-HUB-01 · Cập nhật: 2026-08-17 Surfaces: CON · PAR · DRV · ADM · API Audit + implement: reports/WP-NOTIF-HUB-01.md
Một cửa NotificationsService.dispatch. Vertical không tự appNotification.create. SMS không dùng cho booking (chỉ OTP D-AUTH).
1. Nguyên tắc thống nhất (call · chat · push · inbox)
| Kênh | Khi nào | Không dùng cho |
|---|---|---|
Inbox app_notifications |
Mọi sự kiện người cần đọc lại | GPS ping (ride:location) |
| WS Socket.IO | App đang mở, sự kiện realtime | Promo / social (không có room) |
| Push Expo | App nền / không trong conversation | GPS; chat nếu muted; user tắt prefs (trừ call + bảo mật) |
Chuông gọi call:ring + push high |
Cuộc gọi đến | Booking / order |
| SMS | OTP đăng nhập (D-AUTH) |
Booking, ride, chat, call nhỡ |
Thứ tự quyết định (hiệu quả, tránh spam):
- Sự kiện thuộc lớp telemetry (GPS) → chỉ WS. Dừng.
- Ghi inbox nếu policy
inbox=true. - Emit WS (vertical giữ:
message:new·call:ring·ride:status). - Push nếu: policy.push AND (prefs bật HOẶC
pushEvenIfDisabled) AND (không mute conversation). - Không SMS.
Call và chat không trộn kind: cuộc gọi đến = CALL_INCOMING (không còn SYSTEM). Tin nhắn = CHAT_MESSAGE. Nhỡ = CALL_MISSED.
2. Sơ đồ kiến trúc
flowchart TB
subgraph Events["Sự kiện nghiệp vụ"]
B[Booking hotel/park/rent/med/school]
R[Ride match / pickup / complete]
O[Order FNB/grocery]
C[Chat message]
K[Call ring / missed]
S[Social / bảo mật / admin]
end
H[Notif Hub dispatch]
B --> H
R --> H
O --> H
C --> H
K --> H
S --> H
subgraph Policy["Catalog policy"]
P1[inbox?]
P2[push? mute prefs]
P3[sfx + deepLink]
end
H --> Policy
Inbox[(app_notifications)]
Expo[Expo Push]
WS[Socket.IO rooms]
SMS[eSMS — chỉ OTP ngoài hub]
P1 -->|yes| Inbox
P2 -->|yes| Expo
R -.->|ride:status / GPS| WS
C -.->|message:new| WS
K -.->|call:ring| WS
SMS -.-> AuthOTP[Auth OTP riêng]
3. Sơ đồ tuần tự — đặt xe (mẫu mọi booking)
sequenceDiagram
autonumber
actor U as CON
actor D as DRV
participant API as Nest vertical
participant Hub as Notif Hub
participant WS as Socket.IO
participant Push as Expo
U->>API: POST /rides (hoặc booking)
API->>API: persist Ride/Booking
API->>WS: ride:status CONFIRMED
API->>Hub: dispatch ride.matched CON+DRV
Hub->>Hub: prefs + mute
Hub->>Hub: insert inbox
Hub->>Push: Expo high / channel welife-rides
Push-->>U: banner + sfx notif_ride
Push-->>D: banner (token DRV)
U->>U: tap → deepLink RideDetail
4. Sơ đồ phân luồng call vs chat vs booking
flowchart TD
E[Sự kiện] --> T{Loại?}
T -->|GPS| WSonly[Chỉ WS ride:location]
T -->|Call incoming| CallPath
T -->|Chat message| ChatPath
T -->|Ride status / booking / order| BizPath
T -->|OTP| SmsPath[SmsService eSMS — ngoài hub]
CallPath --> Inbox1[Inbox CALL_INCOMING]
CallPath --> WS1[WS call:ring]
CallPath --> Push1[Push high — kể cả tắt notif chung]
CallPath --> Sfx1[call_ringtone_incoming]
ChatPath --> Inbox2[Inbox CHAT_MESSAGE]
ChatPath --> WS2[WS message:new]
ChatPath --> Mute{Conversation muted?}
Mute -->|yes| NoPush[Không push]
Mute -->|no| Prefs1{prefs on?}
Prefs1 -->|yes| Push2[Push notif_message]
Prefs1 -->|no| NoPush
BizPath --> Inbox3[Inbox RIDE/ORDER/BOOKING]
BizPath --> WS3[WS nếu có room]
BizPath --> Prefs2{prefs on?}
Prefs2 -->|yes| Push3[Push channel theo lớp]
Prefs2 -->|no| SkipPush[Inbox only]
5. Ma trận sự kiện × kênh × surface
| Event | Kind | CON | PAR | DRV | Inbox | Push | WS | SFX |
|---|---|---|---|---|---|---|---|---|
chat.message |
CHAT_MESSAGE | ● | ○ | ○ | ● | nếu không mute | message:new |
notif_message |
call.incoming |
CALL_INCOMING | ● | — | — | ● | luôn high | call:ring |
call_ringtone_incoming |
call.missed |
CALL_MISSED | ● | — | — | ● | ● | call:ended |
call_missed |
ride.matched |
RIDE | ● | — | ● | ● | ● | ride:status |
ride_driver_assigned |
ride.arrived |
RIDE | ● | — | — | ● | ● | ride:status |
ride_driver_assigned |
ride.pickup |
RIDE | ● | — | — | ● | ● | ride:status |
ride_trip_started |
ride.completed |
RIDE | ● | — | ● | ● | ● | ride:status |
ride_trip_completed |
ride.cancelled |
RIDE | ● | — | ● | ● | ● | ride:status |
notif_ride |
ride:location |
— | ● | — | ● | — | — | ride:location |
— |
order.created |
ORDER | ● | ● owner | — | ● | ● | — | notif_order |
order.confirmed |
ORDER | ● | — | — | ● | ● | — | notif_order |
order.cancelled |
ORDER | ● | ● | — | ● | ● | — | notif_order |
booking.hotel.* |
BOOKING | ● | ● org | — | ● | ● | — | notif_order |
booking.parking.* |
BOOKING | ● | ● org | — | ● | ● | — | notif_order |
booking.rental.viewing* |
BOOKING | ● | ● landlord | — | ● | ● | — | notif_order |
booking.medical.created |
BOOKING | ● | ● org | — | ● | ● | — | notif_order |
booking.school.created |
BOOKING | ● | ● org | — | ● | ● | — | notif_order |
booking.homeservice.created |
BOOKING | ● | ● org | — | ● | ● | — | notif_order |
rental.deposit.* |
WALLET | ● | ● | — | ● | ● | — | notif_payment |
social.* |
SOCIAL_* | ● | — | — | ● | ● | — | notif_default |
security.device |
SYSTEM | ● | — | — | ● | luôn | — | notif_urgent |
admin.broadcast |
SYSTEM | ● | ● | ● | ● | ● | — | notif_promo |
| Auth OTP | — | ● | ● | ● | — | — | — | SMS brand WELIFE |
○ = cùng user JWT nếu họ dùng app đó (token chung theo userId).
6. Payload thống nhất (data)
{
"event": "ride.matched",
"kind": "RIDE",
"notificationId": "uuid",
"entityId": "ride-id",
"sfx": "ride_driver_assigned",
"deepLink": {
"tab": "MapTab",
"screen": "RideDetail",
"params": { "rideId": "…" }
},
"surface": "CON"
}
FE CON: tap push / «Mở chi tiết» → openNotifDeepLink (không mở DriverInTrip).
FE DRV: tap push / inbox Cài đặt → InTripScreen (driver/).
OS sound Expo vẫn default (custom .caf phải bundle APK — SOUND-PACK). data.sfx để foreground play sau này.
7. Deep-link theo surface
| Event | CON | DRV |
|---|---|---|
| chat / call | ChatTab · ChatThread / Call (incoming) / CallDetail (missed) | Chat inbox (app tài xế) |
ride.matched |
MapTab · RideDetail | InTripScreen (app tài xế) |
ride.arrived |
MapTab · RideDetail | — (chỉ khách) |
ride.pickup |
MapTab · RideDetail | — (chỉ khách) |
ride.completed / cancelled |
RideDetail | InTrip / lịch sử |
| order.* | MapTab · OrderTrack | — |
| hotel | MapTab · HotelBookings | — |
| parking | MapTab · ParkingBookings | — |
| rental viewing | MapTab · RentalViewings | — |
| rental deposit | MapTab · RentalDeposit | — |
| medical | MapTab · MedicalAppointments | — |
| school | MapTab · SchoolConsultations | — |
| homeservice | MapTab · HomeServicesBookings | — |
| social | SocialTab | — |
| security | AccountTab · AuthSessions | — |
8. Code map
| Vai trò | Path |
|---|---|
| Catalog + policy | api/src/modules/notifications/notif-catalog.ts |
| Dispatch | api/src/modules/notifications/notifications.service.ts |
| REST | GET/PATCH /notifications · POST /push-tokens |
| CON inbox | mobile/src/features/notifications/ |
| CON deep-link | mobile/src/features/notifications/notifDeepLink.ts |
| CON/DRV/PAR token | registerExpoPushToken sau login |
Gọi từ vertical: notifications.dispatch / dispatchFanout (CON + partner owner + driver).
9. Việc còn lại (không chặn hub)
| WP | Việc |
|---|---|
| WP-NOTIF-PRESENCE-01 | PARTIAL — skip push chat nếu đang trong conv: room. Ride room chưa |
| WP-NOTIF-SFX-BUNDLE-01 | Bundle notif_*.caf vào EAS APK |
| WP-NOTIF-ANDROID-CH-01 | IMP trong WP-CHAT-CALL-LOCK-01 (welife-calls/chat/rides/orders) |
| WP-NOTIF-PREFS-CH-01 | Prefs theo kênh (chat / ride / order) thay vì 1 bit |
| WP-NOTIF-VOIP-01 | BLOCKED — cấm thiết bị thật (CallKit / ConnectionService / EAS device) |