mobile/feature-plan.md

Mobile App — Feature Plan

Bối cảnh: booking-mobile/ là React Native + Expo app, chạy WatermelonDB offline-first, OpenAPI codegen cùng API contract với web. Phục vụ Owner + Staff trong 1 app duy nhất (role-based UI switch). Khách hàng dùng web customer portal (/b/:slug), KHÔNG nằm trong scope mobile.

Đọc trước:


0. Prerequisite — Role audit + harden (BLOCKER, làm TRƯỚC mobile)

⚠️ Mobile không build được trước khi fix điểm này. Hiện tại role-based permissions chưa được verify end-to-end trên web + API, nhiều endpoint chặn STAFF khiến flow mobile thiết yếu (walk-in, xem resources, đọc settings) sẽ gãy.

0.0.1 Hiện trạng (phát hiện khi viết plan này)

Vấn đề Chi tiết Impact mobile
Web admin chặn STAFF booking-web/src/app/admin/(dashboard)/layout.tsx: AuthGuard allowedRoles={["ADMIN","OWNER"]} — staff chưa login được vào /admin. Role-based UI chưa từng chạy E2E. Mobile không có web reference pattern để copy
GET /services chặn STAFF service.controller.ts — chỉ OWNER, ADMIN Walk-in gãy — staff không chọn được service
GET /resources chặn STAFF resource.controller.ts — chỉ OWNER, ADMIN Không list được staff khác để owner/staff gán lại booking
GET /tenants/me chặn STAFF tenant.controller.ts — chỉ OWNER, ADMIN BLOCKER — staff không đọc được TenantSettings → không biết business hours, deposit, tz
Resource-scoping booking booking.service.ts:710 có check isAdmin cho transition, KHÔNG thấy filter list theo resourceId = user.resource.id cho STAFF Staff có thể đang thấy mọi booking của salon — vi phạm privacy & UX
0 test role boundary roles.guard.spec.ts chỉ test decorator, không test từng endpoint 403 đúng chỗ. E2E không có scenario STAFF Không biết chỗ nào break

0.0.2 Sprint "Role audit & harden" (1–1.5 tuần)

Deliverables:

  1. Ma trận quyền — file mới docs/architecture/role-matrix.md với bảng endpoint × [ADMIN|OWNER|STAFF|CUSTOMER] × [read|create|update|delete]. Decide dứt khoát từng ô.
  2. Mở quyền STAFF cho read-only endpoint cần thiết: GET /services, GET /resources, GET /tenants/me, GET /loyalty/cards, GET /tenant-customers/:customerId.
  3. Resource-scoping trong service layer:
    • BookingService.list() — nếu role=STAFF → WHERE resourceId = user.resource.id OR resourceId IS NULL (cho self-pick).
    • BookingService.update/updateStatus() — STAFF chỉ được sửa booking mình đang đảm nhận, trừ unassigned → self-pick.
    • TenantCustomerService — staff sửa được notes/tags nhưng không sửa được metrics.
  4. Mở /admin cho STAFF trên web — đổi AuthGuard allowedRoles={["ADMIN","OWNER","STAFF"]}, sidebar filter theo role (staff không thấy Settings/Tax/Payment/Loyalty management), bookings page filter tự động theo resourceId.
  5. E2E test role boundary (Playwright) — tối thiểu:
    • STAFF không list được all resources/services khi endpoint chưa mở.
    • STAFF đã mở quyền thì GET được nhưng không DELETE.
    • STAFF không PATCH được booking của resource khác (403).
    • STAFF self-pick unassigned booking khi có skill matching → OK.
    • STAFF self-pick unassigned booking khi KHÔNG có skill → 403.
    • OWNER override được conflict / double-booking.
    • CUSTOMER token bị reject ở admin endpoints (type claim check).
  6. Unit test role-scoping cho BookingService + TenantCustomerService (happy + 403 path).

Done criteria:

  • docs/architecture/role-matrix.md merged vào docs index.
  • Web /admin login được bằng STAFF account, sidebar đúng, booking list đã filter.
  • 7+ E2E scenarios pass.
  • 0 endpoint còn chặn STAFF nhầm (whitelist đã review toàn bộ controllers).
  • OpenAPI spec regen, booking-web + booking-mobile types sync.

Sau khi xong: mobile copy pattern role switch y hệt web (cùng permission logic, cùng sidebar filter), giảm rủi ro.


0. Nguyên tắc thiết kế

0.1 Role-based, single app

Đăng nhập → JWT trả về role (OWNER/STAFF/ADMIN). App swap navigation stack:

  • OWNER → full access (dashboard, staff, services, settings, payments, reports).
  • STAFF → schedule cá nhân, booking của mình, walk-in, self-pick, check-in, customer quick view. KHÔNG thấy settings, tax, payment config, không sửa được booking của staff khác (trừ khi owner cho).
  • ADMIN (superadmin) → KHÔNG phục vụ trên mobile (memory đã note: admin cần "login as tenant" — dùng web).

0.2 Offline-first (critical cho salon thật)

Salon ở Na Uy nhiều chỗ wifi/signal kém, thao tác mid-appointment cần mượt. WatermelonDB sync cho entities dưới đây:

Entity Offline đọc Offline ghi Ghi chú
Booking ✅ (queue) Ghi offline → mark _status=created/updated, sync khi online. Conflict → server wins + toast.
Resource Owner sửa staff rất ít, cần online.
Service Như trên.
Customer ✅ (create) Walk-in tạo customer mới offline OK, merge khi sync.
TenantCustomer (tags, notes, visitCount) ✅ (update notes/tags) Conflict: merge last-write-wins cho notes, server-wins cho counters.
TenantSettings snapshot Read-only offline — chỉ thay qua desktop hoặc khi online.
ResourceSchedule + ScheduleOverride Tính availability offline nhưng cấm sửa.
TimeOff ✅ (staff request) Offline request, owner approve khi online.
Payment Tuyệt đối online (phải gọi provider). Hiển thị read-only từ cache last-synced, có badge "⚠ Ngoại tuyến — số liệu có thể cũ".
LoyaltyStamp / LoyaltyPointTransaction Đọc cached balance, ghi từ event booking COMPLETED online.

0.3 Các ràng buộc bắt buộc

  • KHÔNG fallback settings — nếu chưa sync TenantSettings, chặn tạo booking (screen "Đang sync dữ liệu salon, vui lòng đợi").
  • Timezone — mọi so sánh giờ làm việc dùng salon timezone (từ tenant.settings.timezone), KHÔNG device local.
  • Money — minor units (øre), format qua shared helper.
  • Error codes — translate từ error.code (domain prefix), KHÔNG show message thô.
  • Zero tolerance — lint + build + type sạch; React Compiler rules như web (memory có feedback_react_compiler).

0.4 Tech stack xác nhận

Layer Lựa chọn Lý do
Framework Expo (React Native) README định sẵn
State React Query + Zustand Đồng bộ với web
DB local WatermelonDB README định sẵn, offline-first
API client OpenAPI codegen Đã có ở web, port pipeline
Navigation Expo Router (file-based) Đồng nhất với Next.js pattern
Auth storage expo-secure-store access + refresh token, never AsyncStorage
Push expo-notifications PRD US-7.3
i18n expo-localization + i18next hoặc react-intl nb-NO primary, sync web translations
Map react-native-maps Native + Apple/Google maps
Image expo-image-picker + upload MinIO Branding + avatar + service
QR expo-barcode-scanner Scan QR check-in
Date time date-fns-tz hoặc luxon Phải dùng salon tz, tránh Date.getHours()

1. Phân lớp MVP

Phase 1 — MVP (cho owner + staff xử lý vận hành hàng ngày)

Mục tiêu: Một salon có thể chạy full-day operation trên mobile mà không cần mở desktop (trừ setup ban đầu + payment config + tax).

Owner

  1. Đăng nhập, xem dashboard (metrics hôm nay + today timeline + upcoming bookings).
  2. Xem full calendar (day view) all staff, booking của tất cả.
  3. Tạo / sửa / cancel booking (multi-service, assign staff, walk-in, from-phone).
  4. Quản lý status (CONFIRMED → ARRIVED → IN_PROGRESS → COMPLETED / CANCELLED / NO_SHOW).
  5. Xem + sửa customer (profile, notes, tags, booking history, loyalty balance).
  6. Xem staff work schedule + approve time-off.
  7. Quick-add / quick-edit service (P0 — name + duration + price + category).
  8. Xem payment list (read-only) + capture/refund basic.
  9. Settings read-only + sửa được: booking policy (toggle walkIn/autoConfirm/bookingMode/cancellationHours), business hours, branding colors.
  10. Push notifications: new online booking, customer check-in, staff self-pick, payment success/failure.

Staff

  1. Đăng nhập → thấy schedule CÁ NHÂN ngày hôm nay.
  2. Xem booking chi tiết (customer, service, notes, giờ, status, payment status).
  3. Update status booking của chính mình (CONFIRMED → ARRIVED khi khách đến, → IN_PROGRESS khi bắt đầu, → COMPLETED khi xong).
  4. Scan QR của khách để mở booking (QR do customer portal gen ở /b/:slug/bookings/:id).
  5. Walk-in quick create (chọn service, auto-assign bản thân, start=now).
  6. Self-pick booking unassigned (nếu bookingMode=allow_unassigned và có skill phù hợp).
  7. Customer quick view (profile, tags, notes, lịch sử booking, loyalty).
  8. Request time-off (owner approve qua notification).
  9. Profile cá nhân — đổi password, phone.
  10. Push notifications: booking mới assign cho mình, walk-in notify owner, schedule change.

Phase 2 — V1 (sau MVP chạy ổn định)

Owner

  • Drag-drop reschedule trên calendar (long-press + drag).
  • Week view calendar (scroll ngang).
  • Staff management: CRUD resource, assign skills, upload avatar.
  • Service management: full CRUD + category + accounting link + metadata + image.
  • Branding upload logo / cover (camera + gallery).
  • Multi-service booking (add item ngay trong drawer).
  • Payment drawer: event log, capture/refund/void/collect remaining.
  • Deposit flow (enable + type + value).
  • Location edit (pin trên map + reverse geocode).
  • TimeOff approve/reject với reason.
  • Onboarding wizard đầu tiên qua mobile (cho user download app trước khi login web).
  • Export booking/revenue report CSV (share qua email/drive).

Staff

  • Self service: xin đổi lịch, xem lương/commission nếu metadata có.
  • Multi-day schedule view (tuần tới).
  • Chat với khách hàng (link về booking) — defer nếu chưa có API.
  • Notifications settings — mute hours.

Phase 3 — V2+ (roadmap xa)

  • Online payment via Vipps (Na Uy native — rất phổ biến).
  • Tap-to-pay (smartphone-as-POS, Apple/Android Tap to Pay SDK).
  • Waitlist (khách đăng ký khi full, staff gọi khi có slot).
  • Review system post-booking.
  • Marketing campaigns (SMS/push cho inactive customers).
  • Calendar sync (iOS/Android native calendar, Google Calendar).
  • Analytics dashboard (booking trends, staff utilization heatmap).
  • Multi-location: swap location trong app (khi có Organization layer).
  • Superadmin "login as tenant" — vẫn không nên đưa lên mobile.

2. Information architecture (IA)

2.1 Navigation — Owner

Tabs (bottom):
├── 🏠 Home              (dashboard)
├── 📅 Calendar          (day view, swap staff filter)
├── ➕ New Booking       (FAB trung tâm, full-screen modal)
├── 👥 Customers
└── ⋯ More
    ├── Staff & Schedule
    ├── Services
    ├── Payments
    ├── Loyalty (view-only)
    ├── Settings
    └── Profile / Logout

2.2 Navigation — Staff

Tabs (bottom):
├── 📅 My Schedule       (today default, swipe cho ngày khác)
├── ➕ Walk-in           (FAB trung tâm, quick-create)
├── 👥 Customers         (read + quick edit notes/tags)
└── ⋯ More
    ├── Unassigned Queue (nếu bookingMode = allow_unassigned)
    ├── Scan QR
    ├── Time-off Request
    └── Profile / Logout

2.3 Screen inventory (ước lượng)

Group Screens Ghi chú
Auth Signin, Forgot Password, Reset Password Owner chỉ dùng email, staff có thể phone
Onboarding (mobile) Welcome, Salon info, Business hours, Services, Staff, Policy, Review V1 — MVP force web
Dashboard (owner) Home Metrics + Today timeline + Upcoming + Quick actions
Calendar Day view, Week view (V1), Booking drawer, ServicePicker, Conflict modal Owner view-all, staff view-self
Booking flow Create booking (multi-step or single screen?), Edit, Status change, Cancel confirm, History audit Single-screen stacked cho mobile
Customers List, Detail (profile + history + loyalty + notes), Create, Edit Tabs: Info / Bookings / Loyalty / Notes
Services List, Create, Edit, Category manage Quick-add inline
Staff (owner) List, Detail (profile + skills + schedule), Create, Edit, Skill picker Industry label dynamic
Work schedule (owner) Grid per-staff, Weekly recurring editor, Override editor, TimeOff approval Native time picker
TimeOff (staff) Request create, My requests, Status view Notif khi approve/reject
Settings Overview (sections), Booking policy, Business hours, Branding, Location, General (read-only most) Tab list → stack detail
Payments List, Detail drawer, Capture/Refund/Void action sheets Native action sheet
Payment config Provider list, Provider detail (read + activate + health-check) NO credentials editing
Loyalty Customer balance (read-only) Card creation → web
Profile Email, phone, password Common
Notifications Feed + mark read + tap deep link In-app feed + push
Scan QR Full-screen scanner → open booking Staff flow

3. Đặc tả chi tiết một số flow mobile-first

3.1 Staff check-in khi khách đến (P0)

[Home / Calendar]
    └─ tap booking → Booking Detail
        ├─ Status badge: CONFIRMED
        ├─ Button "Khách đến" (primary)
        │   └─ Tap → PATCH /bookings/:id/status/ARRIVED → success toast
        │       └─ Refetch booking, animate status badge
        └─ Button "Bắt đầu" (sau ARRIVED)
            └─ PATCH → IN_PROGRESS

Biến thể: Scan QR khách đưa điện thoại → mở trực tiếp Booking Detail → nút "Khách đến" ngay tay.

3.2 Walk-in (P0)

[Walk-in FAB]
    ├─ Chọn service (grid lớn — tap size ≥ 48×48)
    ├─ (nếu owner) chọn staff, (nếu staff) tự gán mình
    ├─ (optional) nhập tên khách + phone — hoặc skip
    └─ Tap "Bắt đầu ngay"
        └─ POST /bookings { source: WALK_IN, startTime: now, status: IN_PROGRESS }
            └─ Toast + push notif owner

Yêu cầu offline: lưu vào WatermelonDB, queue sync, UI hiện ngay trên calendar với badge "⋯ đang đồng bộ".

3.3 Self-pick unassigned (P0 — theo US-4.5)

[Tab More → Unassigned Queue]
    ├─ List bookings resourceId=null, filter theo skill của staff
    ├─ Mỗi card: service + time + customer + duration
    └─ Tap "Nhận" (swipe-right action)
        └─ PATCH /bookings/:id { resourceId: myResourceId }
            ├─ Nếu 409 (ai đó vừa nhận) → toast "Người khác đã nhận rồi"
            └─ Nếu 200 → remove khỏi list + add vào My Schedule

3.4 Owner approve TimeOff (P0)

Push notif "Staff A xin nghỉ 20–22/5" (tap) →
[TimeOff detail] →
    ├─ Hiển thị: staff, range, reason, impact (bookings bị ảnh hưởng)
    └─ 2 button: Approve / Reject + note
        └─ PATCH /time-offs/:id { isApproved: true/false }
            └─ Trigger re-check bookings trong range → hiện danh sách nếu có conflict

3.5 Owner sửa business hours nhanh (P0)

[More → Settings → Business hours]
    ├─ 7 rows (thứ 2 → chủ nhật), mỗi row: toggle mở/đóng + slots list
    ├─ Tap slot → native time picker (start/end)
    ├─ "+ Thêm slot" cho break giữa ngày
    └─ Save → PATCH /tenants/:id { settings: { businessHours } }
        └─ Hỏi: "Áp dụng cho lịch staff?" (applyToStaff boolean)

3.6 Dashboard metrics (P0)

Widgets:

  • Today bookings count (tap → filter calendar hôm nay).
  • Revenue today (tổng paidAmount của booking COMPLETED).
  • Customers new today.
  • Pending bookings (cần confirm thủ công nếu autoConfirm=false).
  • No-show rate 7 ngày.

4. Notifications (push)

4.1 Registration

  1. App khởi động → lấy Expo push token qua expo-notifications.
  2. POST /api/auth/devices với { token, platform, appVersion }.
  3. Lưu localStorage + invalidate khi logout.

4.2 Events (source: domain events từ backend)

Event Audience Deep link Priority
Booking CREATED (source=ONLINE) Owner + assigned staff /bookings/:id P0
Booking CREATED (unassigned) All staff có skill /unassigned P0
Booking STATUS_CHANGE (ARRIVED) Owner /bookings/:id P0
Booking RESCHEDULED (by customer) Owner + staff /bookings/:id P0
Booking CANCELLED (by customer) Owner + staff /bookings/:id P0
Walk-in created Owner /bookings/:id P0
Staff self-pick Owner /bookings/:id P0
TimeOff requested Owner /timeoff/:id P0
TimeOff approved/rejected Staff /timeoff/:id P0
Payment CAPTURED / REFUNDED / FAILED Owner /payments/:id P1
Daily summary (end of day) Owner / P2

4.3 Backend changes cần thiết

  • DeviceToken model (chưa có): { userId, token, platform, lastSeenAt }.
  • NotificationService publish Expo push qua BullMQ queue (infra đã có).
  • Wire vào domain events hiện có (BookingCreated, BookingConfirmed, BookingCancelled, PaymentCaptured, ...).
  • Cron daily summary (end-of-day per tenant timezone).

5. Security & session

Vấn đề Giải pháp
Token storage expo-secure-store (Keychain/Keystore) cho access + refresh
Biometric unlock expo-local-authentication (FaceID/TouchID) — P1, optional
App background lock Blur screen khi backgrounded (compliance salon có thể show khách hàng info)
Role switching JWT trả role → guards navigation, KHÔNG trust client-side only, mọi API call vẫn được backend check
Tenant scope Backend đã enforce tenantId từ JWT + header — mobile KHÔNG cần gửi tenant ID thủ công
Customer data privacy Mask phone/email phần lớn ký tự trong list (chỉ full khi tap detail)
Logout Revoke refresh (POST /auth/logout), clear SecureStore, clear WatermelonDB, force re-sync lần sau
Force logout (tokenVersion) Mọi 401 với code TOKEN_VERSION_MISMATCH → clear + back to login
Offline auth Access token hết hạn offline → chặn mutation, cho phép read cached; online lại → refresh flow

6. Sync strategy (offline-first)

Nguyên tắc: dùng lại 100% REST API hiện có cho web admin. Mobile KHÔNG cần endpoints /api/sync/* riêng. Chỉ bổ sung 2–3 thứ nhỏ (device token, idempotency key) để hỗ trợ retry & push.

Read path — cache response REST hiện có:

  • Login / mở app → gọi song song các list API (GET /bookings?date=today, /resources, /services, /customers, /tenants/me) → lưu WatermelonDB.
  • Refetch định kỳ (30s foreground, expo-background-fetch 15 phút min khi background).
  • Offline: đọc trực tiếp từ WatermelonDB.

Write path — queue + idempotent retry:

  • Mutation offline → lưu pending record trong WatermelonDB + gen idempotencyKey (UUID v7 client-side).
  • Khi online → flush queue bằng chính REST endpoint gốc (POST /bookings, PATCH /bookings/:id/status/:status, ...) kèm header Idempotency-Key.
  • Retry với backoff (1s → 2s → 4s → 8s → 16s → drop + toast).

Tại sao đủ: 1 salon trung bình ~50–200 bookings/ngày → full list pull <200KB, mỗi request ~100ms. Overhead nhỏ, code backend phải viết cũng nhỏ. Incremental sync chỉ đáng đầu tư khi dataset >1000 records/ngày.

6.2 Bổ sung tối thiểu ở backend

Item Mô tả Priority
Idempotency-Key header Áp dụng cho POST /bookings, PATCH /bookings/:id, PATCH /bookings/:id/status/:status. Cache response 24h theo key. Payment context đã có pattern — port sang Booking. P0
POST /api/auth/devices + DELETE /api/auth/devices/:token Đăng ký / huỷ Expo push token, gắn với userId + role + tenantId. P0
(optional) ?updatedAfter=<iso> query param cho list endpoints Pull delta thay vì full list. Chỉ làm khi profiling cho thấy payload quá lớn. P2

6.3 Conflict resolution (minimal)

Dùng updatedAt + If-Unmodified-Since hoặc ETag (API đã có updatedAt):

  • Client gửi If-Unmodified-Since: <cached_updatedAt> khi PATCH.
  • Server trả 409 nếu bản ghi đã bị sửa sau thời điểm đó.
  • Client → toast "Dữ liệu đã thay đổi, refetch" → refetch + cho user thao tác lại.

Default rule: server always wins. Client không tự merge — refetch + show lại.

Ngoại lệ: Customer.notes, TenantCustomer.tags → client merge last-write-wins vì ít conflict thực tế.

6.4 Background sync

  • Foreground: React Query refetchInterval: 30_000 cho danh sách chính.
  • Backgrounded: expo-background-fetch 15 phút (iOS min).
  • Kết nối lại sau offline: NetInfo listener → flush pending queue + invalidate queries.

6.5 Sơ đồ dòng dữ liệu

[Mobile UI]
    │  write
    ▼
[Mutation] ──online?── yes ─► [POST /api/bookings + Idempotency-Key]
    │                             │
    │ no                          └─ 200 → update cache
    ▼                             └─ 409 → refetch + retry
[WatermelonDB queue]
    │
    │  NetInfo → online
    ▼
[Flush loop] → same POST với cùng Idempotency-Key

7. Gate / chặn hành động

Tình huống Chặn UX
TenantSettings chưa sync ❌ Không cho tạo booking Full-screen loading "Đang đồng bộ dữ liệu salon"
Không có skill khi self-pick ❌ Ẩn nút "Nhận" Staff không thấy booking đó trong queue
allowDoubleBooking=false + conflict detect ⚠️ Cảnh báo, chỉ owner override Modal confirm "Booking này trùng với X — vẫn tiếp tục?"
autoConfirm=false + booking mới ℹ️ Status = PENDING Badge "Chờ xác nhận" + CTA confirm
depositEnabled=true nhưng không có payment config ❌ Không cho enable trong settings Warning icon + link sang payment config
Offline + action yêu cầu online (capture, refund, change password) ❌ Disable button Tooltip "Cần kết nối mạng"

8. Test strategy

8.1 Test pyramid

  • Unit: utility functions (format, tz conversion, sync conflict logic). Vitest hoặc Jest + React Native Testing Library.
  • Integration: navigation flows + API mocks. MSW (mock service worker).
  • E2E: Detox hoặc Maestro — smoke test critical flows (login → new booking → status change).

8.2 Offline tests

  • Simulated airplane mode khi walk-in → sync → verify.
  • Conflict simulation: 2 devices self-pick cùng lúc.

8.3 Coverage target

80%+ như web (memory: feedback_commit_checklist).


9. Milestone / timeline ước lượng

Phase Nội dung Ước tính
M-1 Role audit (xem §0) Ma trận quyền, mở quyền STAFF các read endpoint, resource-scoping service layer, mở /admin cho STAFF, E2E role boundary 1–1.5 tuần (blocker)
M0 Setup Expo app skeleton, codegen pipeline, auth login, SecureStore, i18n, theme sync branding 3–5 ngày
M1 Infra WatermelonDB models, sync endpoints (API side), push token register 5–7 ngày
M2 Owner MVP Dashboard, Calendar day view, Booking CRUD + status, Customers read, Services quick-edit, Settings P0 10–14 ngày
M3 Staff MVP My Schedule, Walk-in, Status change, Self-pick, Scan QR, TimeOff request, Customer quick view 7–10 ngày
M4 Notifications Expo push wire-up, event bus → push listener, in-app feed 3–5 ngày
M5 Polish + E2E Offline flows, conflict test, error boundaries, accessibility, Detox/Maestro smoke, store build 5–7 ngày

Tổng MVP: ~7–9.5 tuần solo developer (role audit 1–1.5 tuần + mobile 6–8 tuần), song song với backend enhancements (device token, push worker, idempotency key).


10. Backend dependencies (cần chuẩn bị trước khi mobile build)

Tin tốt: 90% REST API hiện tại của web admin reuse được nguyên cho mobile (cùng OpenAPI, cùng auth, cùng tenant scope). Chỉ cần bổ sung những mục dưới đây.

Task Ai làm Status Priority
Role audit & harden (xem §0) — ma trận quyền, mở quyền STAFF cho read endpoints, resource-scoping service layer, mở /admin cho STAFF, E2E role boundary Backend + Web P0 — BLOCKER
DeviceToken model + POST/DELETE /api/auth/devices Backend P0
Expo push notification service (BullMQ worker) Backend P0
Event bus → push dispatcher (listen 5–6 event types) Backend P0
Idempotency-Key header cho POST /bookings + PATCH /bookings/:id + PATCH /bookings/:id/status/:status Backend ❌ (payment đã có) P0
OpenAPI spec stability (no breaking change trong MVP sprint) Backend P0
REST API hiện tại (bookings, resources, services, customers, tenant, settings, payments) Backend
/api/auth/login trả role + tenantId Backend
updatedAfter=<iso> query param cho list endpoints (nice-to-have) Backend P2
SMS notifications (US-7.1, 7.2) Backend P1 (mobile không phụ thuộc)
Multi-location (Organization layer) Backend P2 (V2)

11. Tham khảo pattern có sẵn trên web để port

Web helper/hook Mobile tương đương
useTenant() Giống, dùng React Query + cache từ Watermelon
useResourceLabel() (industry-aware) Port y hệt
useCurrency(), useFormatMoney() Port y hệt
lib/booking-status.constants.ts (transitions, colors) Share qua codegen hoặc copy
lib/timezone.ts (salon tz utilities) Port dùng luxon hoặc date-fns-tz
components/form/* (FormField, PhoneField, MoneyField) Tạo bản native tương đương
components/ui/ConfirmDialog react-native-modal + cùng API
useFormMutation Giống (React Query mutation wrapper)
useToast() react-native-toast-message hoặc custom

💡 Một số helper có thể extract thành package shared (@booking/shared) nếu monorepo. Hiện là multi-repo nên copy + cùng DTO types qua OpenAPI codegen là đủ.


12. Câu hỏi mở — cần decision trước khi bắt đầu

  1. Auth login: chỉ email/password, hay cho phép staff login bằng phone + OTP? PRD không nói rõ.
  2. Walk-in customer: bắt buộc nhập phone hay optional? Hiện public flow là optional.
  3. Self-pick race: backend đã có optimistic lock? Nếu chưa → cần thêm If-Match / version check trên PATCH booking.
  4. Notification sound/vibration: có cần per-event preference không? P1 là đủ.
  5. Dark mode: salon hay sáng sủa, nhưng đêm staff xử lý booking có thể tối. Theme system nên follow branding primaryColor.
  6. Min OS: iOS 14+ / Android 8+? Tuỳ Expo SDK version (SDK 51+ yêu cầu iOS 13.4+ / Android 7+).
  7. Payment in mobile: có ý định cho owner nhập credentials KHÔNG BAO GIỜ trên mobile (confirmed). Nhưng capture / refund / void từ mobile nên cho, vì thường xử lý ngay khi khách trả tiền.
  8. Admin impersonate (login as tenant): defer, chỉ desktop.
  9. Waitlist / prepaid packages: roadmap (docs/progress) nói "Long-term". Mobile nhận lịch scope V2.
  10. Commission tracking: Resource.metadata.commission đã có. Có show cho staff xem tiền hoa hồng không? → đề xuất P1 kèm toggle owner bật/tắt per-tenant.

13. Tóm tắt — bắt đầu từ đâu?

Đề xuất sprint 1 (2 tuần) để chứng minh vòng khép kín:

  1. Backend: DeviceToken + POST /api/auth/devices; 1 BullMQ worker gửi Expo push; listen 1 event (BookingCreated); thêm Idempotency-Key cho POST /bookings.
  2. Mobile: login + SecureStore + codegen + WatermelonDB 2 model (Booking, Resource).
  3. Mobile: gọi GET /api/bookings?date=today → cache WatermelonDB → My Schedule đọc offline được.
  4. Mobile: offline walk-in → queue POST với idempotency key → flush khi online.
  5. Mobile: nhận push khi booking mới → deep link vào Booking Detail.

Xong sprint 1 → có framework để mở rộng nhanh. Các phase sau chủ yếu là thêm UI + lặp lại pattern read/write này cho entities khác.


14. Keep in sync

Doc này update song song khi:

  • Thêm/sửa TenantSettings field mới (link admin-settings-inventory.md).
  • Backend thêm domain event cần push.
  • Thay đổi sync protocol.
  • Release mobile version (ghi trong docs/progress/changelog.md).