operations/troubleshooting.md

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

  1. 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.
  2. Mở flow doc linked để hiểu sequence kỳ vọng.
  3. Inspect code path ở file listed. Line range không pin — search symbol nếu file moved.
  4. Check common causes theo thứ tự. Sắp xếp theo tần suất.
  5. 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_PROVIDERpayment_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=nullcustomerPhone=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) EmailProviderLogEmailProvider (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=truedepositEnabled=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 ACTIVEask (/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:

  1. GitNexus querygitnexus_query({query: "<symptom keywords>"}) tìm execution flows ranked by relevance. Nhanh hơn grep cho cross-cutting concerns.
  2. Outbox + inbox audit — bug payment hầu như luôn show là 1 stuck row trong domain_event_outbox (no published_at) hoặc payment_webhook_inbox (no processed_at). Query cả 2 trước.
  3. 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).
  4. Mở DEFERRED.md — nhiều symptom "kỳ lạ" hoá ra là known defer (e.g. EmailProviderLogEmailProvider nên không có email nào thật sự rời inbox).
  5. 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.