Troubleshooting Runbook
Index theo symptom cho hệ thống đang chạy. Tìm row khớp với cái salon / customer đang nhìn thấy → follow link sang flow doc → drill xuống code path. Mỗi row chỉ ra 1-2 nguyên nhân phổ biến nhất cần loại trừ trước khi điều tra sâu.
Khi anh fix một bug class chưa có ở đây, add 1 row. Runbook là living documentation của "những thứ thật sự đã hỏng trong prod".
Cách dùng
- Match symptom ở bảng dưới. Càng cụ thể càng tốt — "booking sai" match mọi thứ; "PENDING booking không bao giờ confirm sau khi customer trả tiền" match đúng 1 row.
- Mở flow doc linked để hiểu sequence kỳ vọng.
- Inspect code path ở file listed. Line range không pin — search symbol nếu file moved.
- Check common causes theo thứ tự. Sắp xếp theo tần suất.
- Nếu không cause nào fit, copy symptom vào row mới TRƯỚC khi ship fix để người tiếp theo tìm nhanh hơn.
Vòng đời Booking
| Triệu chứng | Flow doc | Code path | Nguyên nhân phổ biến |
|---|---|---|---|
Booking đứng yên ở PENDING sau khi customer trả tiền trên Bambora |
05-payment-driven.md §2 |
booking-api/src/core/booking/on-payment-authorized.listener.ts |
(1) Webhook stuck ở payment_webhook_inbox (verify processed_at đã set). (2) Outbox publisher throw — check domain_event_outbox.last_error. (3) Cross-tenant guard reject — event.tenantId !== booking.tenantId |
| Booking auto-cancel ngay sau khi customer trả tiền | 05-payment-driven.md §3 |
booking-api/src/core/booking/on-payment-settled-negative.listener.ts |
(1) PaymentExpired đến trước PaymentAuthorized (race; outbox order). (2) Bambora callback hash mismatch khiến webhook retry as failed. (3) Lead-time > 7d + depositEnabled (P1-11 hard cap đáng lẽ phải reject ở settings save) |
BOOKING_NOT_RETRY_ELIGIBLE (422) khi customer click Retry |
05-payment-driven.md §3 |
booking-api/src/core/public-booking/public-booking.controller.ts retryPayment |
(1) Booking đã CANCELLED (cap exhausted giữa FE poll + click). (2) depositStatus không phải RETRY_PENDING — projection lag hoặc AUTHORIZED đã đến |
BOOKING_PAYMENT_RETRY_EXHAUSTED sau ít hơn 3 retry visible |
05-payment-driven.md §3 |
booking-api/src/core/booking/on-payment-settled-negative.listener.ts PAYMENT_RETRY_CAP |
Có nhiều Payment row tồn tại cho booking mà customer không tạo — check orphan rows từ idempotencyKey cũ. Cap đếm TẤT CẢ FAILED rows |
BOOKING_CANCELLATION_TOO_LATE (422) khi admin cancel |
03-cancel.md §3 |
booking-api/src/core/booking/booking.service.ts validateCancellationWindow |
Admin/owner quên gửi force=true + reason (P0-1). FE đáng lẽ auto-mở ForceOverrideModal — nếu không, check mutation onError wiring |
Customer không tự cancel được (403 BOOKING_NOT_IN_CUSTOMER_SCOPE) |
03-cancel.md §7 |
booking-api/src/core/public-booking/public-booking.controller.ts cancelBooking |
Booking là guest booking (customerId=null) — không có ownership để verify. Customer phải gọi salon (P1-2 dialog đáng lẽ phải hiện) |
| Walk-in booking show "Awaiting payment" hoặc không tạo Payment | 01-create.md §4 |
booking-api/src/core/booking/booking.service.ts createWalkIn |
(1) paymentMode không stamp trên event — verify OnBookingCreated short-circuit trên IN_PERSON. (2) Event payload thiếu — check outbox |
| Phone booking redirect customer sang PSP unexpected | 01-create.md §4 |
booking-api/src/core/booking/booking.service.ts create |
dto.source !== BookingSource.PHONE — staff không pick "Phone" trong source dropdown, nên derivation không fire (P1-3) |
Vòng đời Payment
| Triệu chứng | Flow doc | Code path | Nguyên nhân phổ biến |
|---|---|---|---|
| Customer redirect sang Bambora và stuck — checkout không bao giờ load | 05-payment-driven.md §2 |
booking-api/src/core/payment/application/commands/initiate-payment.handler.ts |
(1) Payment.metadata.checkoutUrl empty — Bambora createSession không return URL. (2) NO_ACTIVE_PROVIDER — payment_configs.is_active bị tắt. (3) Bambora 5xx khi init — adapter throw ProviderError (5xx đã retry 3x trong adapter) |
| Webhook return 200 nhưng booking không update | 05-payment-driven.md |
booking-api/src/core/payment/infrastructure/webhooks/process-webhook-inbox.service.ts |
(1) payment_webhook_inbox.failure_message set — apply step throw. (2) findByProviderRef + findByProviderSessionId đều miss — txnid/orderid mismatch. (3) Hash verify fail — verifyCallbackHash sai MD5 key |
PaymentExpired fire cho booking mà customer trả tiền tuần trước |
07-edge-cases.md §2b |
booking-api/src/core/payment/application/expiry/authorization-expiry.service.ts |
Bambora auth hold 7 ngày đã lapsed — P1-11 hard cap đáng lẽ phải prevent maxBookingDaysInAdvance > 7 khi depositEnabled=true; nếu user lands here, booking auto-cancel với reason AUTHORIZATION_EXPIRED |
Refund return 422 INVALID_STATE_TRANSITION |
— | booking-api/src/core/payment/domain/payment.ts refund |
Payment status là AUTHORIZED (chưa CAPTURED) — refund cần captured funds. Dùng Void thay. Hoặc: cumulative refund vượt captured amount |
| Customer không nhận SMS refund/void | 05-payment-driven.md §3 |
booking-api/src/core/booking/on-payment-notification.listener.ts |
(1) Booking có customer.phone=null VÀ customerPhone=null (guest booking không phone). (2) NotificationService.enqueue fail silently — fire-and-forget by design. (3) SMS provider report delivered nhưng carrier drop |
| Customer không nhận email/SMS retry sau PaymentFailed | 05-payment-driven.md §3 |
booking-api/src/core/booking/on-payment-failed-retry-notification.listener.ts |
(1) Booking đã CANCELLED (cap exhausted, listener short-circuit). (2) Booking không có contact info. (3) EmailProvider là LogEmailProvider (default) — production cần EMAIL_PROVIDER=SMTP + adapter (DEFER p1-4-smtp-vendor) |
Deposit + projection
| Triệu chứng | Flow doc | Code path | Nguyên nhân phổ biến |
|---|---|---|---|
depositStatus đứng yên PENDING lâu sau khi Payment authorized |
05-payment-driven.md §4 |
booking-api/src/core/booking/on-payment-state-projection.listener.ts |
(1) Cross-tenant guard reject (event tenant ≠ booking tenant). (2) bookingId null trên event payload — ad-hoc payment, projection skip. (3) Outbox stuck trước khi publisher reach listener |
depositStatus = RETRY_PENDING nhưng không show button Retry payment |
05-payment-driven.md §3 |
booking-web/src/app/(customer)/account/BookingsSection.tsx BookingCard |
FE expect status=PENDING && depositStatus=RETRY_PENDING. Nếu status race nhanh sang CANCELLED, button hide intentionally |
Settings + tenant
| Triệu chứng | Flow doc | Code path | Nguyên nhân phổ biến |
|---|---|---|---|
Tenant settings save return TENANT_SETTINGS_AUTOCONFIRM_DEPOSIT_CONFLICT |
01-create.md §3 |
booking-api/src/core/tenant/tenant-settings.validation.ts validateSettingsCombination |
Cả autoConfirm=true và depositEnabled=true — mutually exclusive (P1-5). FE BookingPolicyEditor đáng lẽ disable mỗi toggle khi cái kia on |
Tenant settings save return TENANT_SETTINGS_DEPOSIT_LEAD_TIME_CONFLICT |
07-edge-cases.md §2b |
booking-api/src/core/tenant/tenant-settings.validation.ts validateSettingsCombination |
depositEnabled=true + maxBookingDaysInAdvance > 7 (P1-11). Bambora auth hold = 7 ngày max — gì dài hơn sẽ auto-cancel CONFIRMED bookings |
Onboarding step accept settings mà regular update reject |
— | booking-api/src/core/onboarding/onboarding.service.ts saveStep |
Validator chạy trên patch only, không phải merged state — phải gọi validateSettingsCombination(merged) không phải (patch) |
Auth + customer portal
| Triệu chứng | Flow doc | Code path | Nguyên nhân phổ biến |
|---|---|---|---|
Customer 401 trên /public/.../cancel hoặc /payment/retry sau khi token expire |
auth-architecture |
booking-web/src/lib/api-client.ts CUSTOMER_AUTHED_PUBLIC_PATHS |
Path regex không match — thêm public-but-customer-authed endpoint mới yêu cầu update list này, không thì token refresh skip |
| Cross-tenant data show sau sign-in / tenant switch | — | booking-web/src/lib/api-client.ts + AuthContext |
queryClient.clear() không gọi trên auth transition — leak qua tenant. Xem memory feedback_queryclient_clear_on_auth |
Custom domain
| Triệu chứng | Flow doc | Code path | Nguyên nhân phổ biến |
|---|---|---|---|
Domain kẹt PENDING / verify luôn fail |
custom-domain.md §5 |
booking-api/src/core/tenant-domain/domain-verification.service.ts |
(1) TXT _glamvoo-verify.<host> chưa có hoặc sai token (check trước). (2) Apex nhưng CUSTOM_DOMAIN_SERVER_IPS chưa set → nhánh A-record không chạy. (3) DNS chưa propagate (TTL). (4) CNAME trỏ sai (phải connect.glamvoo.com). lastError trong tenant_domains ghi lý do |
Mở mysalon.com → lỗi cert / "not secure" |
custom-domain.md §6 |
caddy/Caddyfile on_demand_tls |
(1) Domain chưa ACTIVE → ask (/internal/tls-allow) trả 404 → Caddy từ chối cấp cert. (2) Port 80 bị chặn (ACME http-01 fail). (3) connect.glamvoo.com không trỏ về IP Caddy. Check docker logs caddy |
Mở custom domain → trang /error-404 |
custom-domain.md §7 |
booking-web/src/proxy.ts handleCustomDomain |
(1) Không có TenantDomain ACTIVE cho host → resolveSlug trả null. (2) Proxy cache TTL 60s chưa hết sau khi verify (đợi ≤60s). (3) NEXT_PUBLIC_PLATFORM_DOMAINS lỡ chứa host khách → bị coi là platform |
Sau thanh toán, khách bị đẩy về glamvoo.com thay vì custom domain |
custom-domain.md §9 |
booking-api/src/core/tenant-domain/public-url.helper.ts + booking.service.ts activeDomainHostname |
Domain chưa ACTIVE lúc tạo booking → return_url fallback về PUBLIC_WEB_URL/b/<slug>. Verify domain ACTIVE trước khi khách book |
/admin trên custom domain ra 404 |
custom-domain.md §7 |
booking-web/src/proxy.ts |
By design — admin chỉ truy cập qua platform domain (glamvoo.com/admin). Không phải bug |
Sau ./deploy.sh, Caddy không boot / cả site down |
docker-deploy.md §2.4 Rollback |
caddy/Caddyfile + .env |
(1) DOCS_BASIC_AUTH_HASH rỗng/sai escape → block basic_auth lỗi → Caddyfile fail load. (2) Port 80 chặn → ACME fail. (3) Domain trong Caddyfile không trỏ về VPS (vd app-dev). Rollback: git checkout <caddy-commit>~1 -- docker-compose.prod.yml deploy.sh && docker compose up -d --remove-orphans → nginx về lại |
Investigation toolkit
Khi bảng chưa có symptom của anh, work qua thứ tự sau:
- GitNexus query —
gitnexus_query({query: "<symptom keywords>"})tìm execution flows ranked by relevance. Nhanh hơn grep cho cross-cutting concerns. - Outbox + inbox audit — bug payment hầu như luôn show là 1 stuck row trong
domain_event_outbox(nopublished_at) hoặcpayment_webhook_inbox(noprocessed_at). Query cả 2 trước. docs/flows/booking-status-flow.md— state machine + valid transitions. Nếu transition user mô tả không có trên diagram, user sai (hoặc anh tìm thấy bug).- Mở
DEFERRED.md— nhiều symptom "kỳ lạ" hoá ra là known defer (e.g.EmailProviderlàLogEmailProvidernên không có email nào thật sự rời inbox). - Add 1 row vào file này TRƯỚC khi ship fix. Anh tương lai sẽ cảm ơn anh hiện tại.