SMS eSMS — Ma trận · Sơ đồ · Logic audit (đầy đủ)
Domain: D-AUTH · WP: WP-AUTH-SMS-01 · WP-FE-AUTH-SMS-ESMS-01
Ngày: 2026-08-14 · Surface: CON + API + OWNER
E2E live: chưa chạy (docs-only) — OWNER chọn done | fail | blast ok + SĐT
Liên quan: STATUS · OWNER-GATES · e2e preflight · FE wire · ready keys
1. Verdict ma trận (snapshot)
| Lớp |
Thành phần |
Status |
Evidence |
| Product |
Login SĐT brand WELIFE |
WIRED FE |
PhoneFirebaseScreen → Nest |
| API |
POST /api/v1/auth/otp/send |
LIVE prod |
validation 400 phone invalid |
| API |
POST /api/v1/auth/otp/verify |
LIVE |
JWT + findOrCreateByPhone |
| Provider |
VPS SMS_PROVIDER=esms |
READY |
ESMS keys SET · brand WELIFE |
| Provider |
Local SMS_PROVIDER=console |
DEV |
devOtp / log — không trừ credit |
| FE API URL |
mobile/.env.local → prod |
SET |
https://api.minhtien.cloud/api/v1 |
| OWNER e2e |
Nhận SMS + login Home |
PENDING |
chờ done / fail / blast |
| Machine queue |
STUB→IMP |
EMPTY |
không invent WP |
Điểm dừng agent: không blast OTP trừ khi OWNER gửi blast ok + SĐT E.164.
2. Ma trận traceability D-AUTH · SMS
| ID |
Layer |
Path / Endpoint |
Role |
Status |
| SCR-AUTH-PHONE |
CON UI |
mobile/src/features/auth/screens/PhoneFirebaseScreen.tsx |
USER |
IMP eSMS |
| SCR-AUTH-GATE |
CON nav |
AuthGate.tsx entry=phone |
USER |
IMP |
| CTX-AUTH |
CON |
AuthContext.sendPhoneOtp / verifyPhoneOtp |
USER |
IMP |
| API-CLIENT |
CON |
AuthApi.sendOtp / verifyOtp |
USER |
IMP |
| EP-SEND |
API |
POST /auth/otp/send |
public + throttle 5/min |
IMP |
| EP-VERIFY |
API |
POST /auth/otp/verify |
public + throttle 10/min |
IMP |
| SVC-AUTH |
API |
AuthService.sendOtp / verifyOtp |
— |
IMP |
| SVC-SMS |
API |
SmsService.sendEsms |
— |
IMP |
| STORE-OTP |
API |
Redis/otpStore · TTL · lock |
— |
IMP |
| DTO |
API |
SendOtpDto @IsPhoneNumber('VN') |
— |
IMP |
| ENV-PROD |
Ops |
SMS_PROVIDER=esms · ESMS_* · brand WELIFE |
OWNER |
READY |
| ENV-LOCAL |
Ops |
SMS_PROVIDER=console · OTP_DEV_CODE |
DEV |
SET |
| E2E-OWNER |
Gate |
App prod → SMS → Home |
OWNER |
PENDING |
Surfaces: CON (consumer phone) · API shared · DRV/PAR có thể reuse cùng endpoint với surface (chưa e2e riêng).
3. Sơ đồ kiến trúc (happy path)
sequenceDiagram
autonumber
actor U as User CON
participant App as PhoneFirebaseScreen
participant Ctx as AuthContext
participant API as Nest /auth/otp
participant Store as OTP Store Redis
participant SMS as SmsService eSMS
participant ES as rest.esms.vn
U->>App: Nhập SĐT +84… · Gửi OTP
App->>Ctx: sendPhoneOtp(+84…)
Ctx->>API: POST /otp/send {phone, surface:CONSUMER}
API->>API: validate VN phone · rate-limit · lock?
API->>Store: set(phone, code, TTL)
API->>SMS: sendOtp(phone, code)
SMS->>ES: SendMultipleMessage_V4 (Brandname=WELIFE)
ES-->>SMS: CodeResult=100 · SMSID
SMS-->>API: {provider:esms, sent:true}
API-->>Ctx: {success, smsProvider, smsSent, expiresInSec}
Ctx-->>App: hint WELIFE · step=code
ES-->>U: SMS OTP brand WELIFE
U->>App: Nhập 6 số · Xác nhận
App->>Ctx: verifyPhoneOtp(+84…, otp)
Ctx->>API: POST /otp/verify
API->>Store: get · match · del · clearFailures
API->>API: findOrCreateByPhone · issueTokens
API-->>Ctx: accessToken · refreshToken · user
Ctx->>Ctx: setTokens · UsersApi.me · chat/push
Ctx-->>App: session ready → Home
4. Sơ đồ quyết định OWNER reply
flowchart TD
A[OWNER thử e2e trên app prod] --> B{Kết quả?}
B -->|SMS eSMS done| C[PASS gate SMS]
B -->|SMS eSMS fail: lỗi| D[Phân loại fail]
B -->|blast ok + SĐT| E{Agent có SĐT E.164?}
B -->|docs only / chưa thử| F[Giữ PENDING · không blast]
C --> C1[Cập nhật STATUS SMS = PASS]
C --> C2[Next: Pay hoặc MoMo]
D --> D1{Fail ở đâu?}
D1 -->|Gửi OTP API lỗi| D2[503 esms / 429 lock / 400 phone]
D1 -->|Không nhận SMS| D3[Portal eSMS · brand · credit · SĐT]
D1 -->|Sai OTP / hết hạn| D4[TTL · attempts · clock]
D1 -->|Login xong nhưng không Home| D5[FE applySession / me]
E -->|Có + blast ok| E1[Agent POST /otp/send 1 lần]
E -->|Thiếu SĐT| E2[Hỏi lại SĐT · không gửi]
E1 --> E3[OWNER đọc SMS · verify trên app hoặc curl]
E3 --> B
5. Logic audit — từng bước
5.1 FE send
| Bước |
Logic |
Pass nếu |
Fail nếu |
| 1 |
National ≥9 digits → +84 + digits |
canSend=true |
Nút disabled |
| 2 |
sendPhoneOtp(e164) |
200 + success |
Network / 4xx / 5xx → error text |
| 3 |
Hint |
smsProvider=esms / devOtp local |
Không có hint nhưng vẫn vào step code nếu 200 |
| 4 |
UI |
step=code · resend 59s |
Stay phone |
Không còn: firebaseStartPhoneLogin trên màn phone.
5.2 API send (AuthService.sendOtp)
| Check |
Rule |
HTTP |
Prod + OTP_DEV_CODE |
Cấm |
403 |
| Lock remaining |
Redis lock |
429 + lockRemainingSec |
| Phone DTO |
@IsPhoneNumber('VN') |
400 |
| Throttle |
5 / 60s |
429 |
| Code |
Random 6 số (prod) hoặc OTP_DEV_CODE (non-prod) |
— |
| Store |
otpStore.set(phone, {code, expires, surface}, ttl) |
— |
| SMS |
SmsService.sendOtp |
503 nếu provider fail |
| Response |
smsProvider · smsSent · otpBackend · không devOtp trên prod |
200 |
5.3 eSMS adapter (SmsService.sendEsms)
| Field |
Value |
| Phone normalize |
8490… (normalizeVnPhone) |
| Content |
WELIFE OTP: {code}. Hieu luc {TTL}s… |
| Brandname |
ESMS_BRAND_NAME / WELIFE |
| SmsType |
ESMS_SMS_TYPE default 2 |
| Success |
CodeResult === "100" |
| Fail |
HTTP non-OK / JSON bad / CodeResult≠100 → 503 |
5.4 API verify (AuthService.verifyOtp)
| Check |
Rule |
HTTP |
| Lock |
Đang khóa |
429 |
| Match |
record tồn tại · chưa hết hạn · code===otp |
else 401 + đếm fail |
| Max attempts |
OTP_MAX_ATTEMPTS (default 5) |
lock → 429 |
| Success |
del OTP · clearFailures · findOrCreateByPhone · issueTokens |
200 JWT |
| Surface |
CONSUMER default · driver/partner flags trong body |
— |
5.5 FE verify
| Bước |
Logic |
verifyPhoneOtp |
AuthApi.verifyOtp → applySession (tokens · me · chat · push) |
| Success |
AuthGate thấy user → MainTabs |
| Fail |
error trên màn OTP |
6. Ma trận outcome OWNER (điền khi có kết quả)
| Outcome message |
Agent hành động |
STATUS SMS |
Next |
SMS eSMS done |
Ghi PASS · đóng gate SMS e2e |
PASS |
Pay / MoMo |
SMS eSMS fail: … |
Phân loại §5 · đề xuất fix (không blast) |
FAIL + root cause |
OWNER retry / fix env |
blast ok + +84… |
1 lần POST /otp/send · báo smsProvider/smsSent · không log OTP |
PARTIAL→chờ verify |
OWNER nhập OTP |
| docs-only / chưa thử |
Giữ PENDING · cập nhật docs này |
PENDING |
OWNER thử app |
Hiện tại: docs-only → PENDING.
7. Ma trận fail triage (checklist)
| Triệu chứng |
Layer |
Kiểm tra |
| «phone must be a valid phone number» |
DTO |
E.164 +84 + 9 số |
| 429 SĐT tạm khóa |
Store |
Đợi lockRemainingSec |
| 503 Gửi SMS thất bại (esms) |
eSMS |
VPS keys · CodeResult · credit portal |
| 200 nhưng không có SMS |
eSMS / telco |
Brand WELIFE · SmsType · SĐT blacklist |
smsProvider=console trên app |
Env |
App đang trỏ local API → đổi prod URL |
| Sai OTP |
User / TTL |
TTL 300s · không copy nhầm |
| Verify OK nhưng kẹt splash |
FE |
Tokens · UsersApi.me · AuthGate |
8. Env split (audit bảo mật)
| Key |
api/.env / VPS |
mobile EXPO_PUBLIC |
Git |
SMS_PROVIDER |
✅ |
❌ |
example only |
ESMS_API_KEY / SECRET |
✅ |
❌ |
cấm |
ESMS_BRAND_NAME |
✅ WELIFE |
❌ |
example |
OTP_DEV_CODE |
local only · cấm prod |
❌ |
— |
EXPO_PUBLIC_API_URL |
— |
✅ prod URL |
— |
9. So sánh cũ vs mới
|
Trước (Firebase Phone) |
Sau (WP-FE-AUTH-SMS-ESMS-01) |
| Gửi OTP |
Firebase Auth |
Nest → eSMS |
| Brand SMS |
Google / Firebase |
WELIFE |
| Verify |
Firebase idToken → /auth/firebase |
Nest OTP → JWT trực tiếp |
| Phụ thuộc bridge |
EXPO_PUBLIC_FIREBASE_BRIDGE |
Không (phone entry luôn) |
| Local |
Firebase test numbers |
console + devOtp |
Email / Google vẫn có thể dùng Firebase (tuỳ bridge) — không đụng trong WP này.
10. Agent runbook (sau khi OWNER reply)
IF message contains "SMS eSMS done":
→ STATUS SMS = PASS · report PASS · stop blast
ELIF message starts "SMS eSMS fail":
→ map symptom → §7 · propose fix · no invent WP
ELIF message contains "blast ok" AND phone matches /^\+84\d{9,10}$/:
→ ONE POST prod /auth/otp/send · report smsProvider/smsSent · never print OTP
ELSE:
→ ask clarification · no send
11. File đụng (code đã IMP)
mobile/src/features/auth/screens/PhoneFirebaseScreen.tsx
mobile/src/features/auth/AuthContext.tsx
mobile/src/features/auth/AuthGate.tsx
mobile/src/features/auth/LoginScreen.tsx
mobile/src/services/api.ts
api/src/modules/auth/auth.controller.ts · dto/auth.dto.ts · sms.service.ts · auth.service.ts
Footer
Domain: D-AUTH
WP: WP-AUTH-SMS-01 · WP-FE-AUTH-SMS-ESMS-01
Surfaces: CON · API · OWNER
Status: WIRED · e2e PENDING (docs-only)
Local: N/A (audit docs)
Next: SMS eSMS done | fail:… | blast ok + +84…