Communication — Case thường gặp
Cách đọc mỗi case: bắt đầu từ triệu chứng/câu hỏi, sau đó đọc behavior đúng, cách kiểm tra ở UI/API/dữ liệu/code, source anchor và Ví dụ thực tế. Các case ở đây ưu tiên behavior đang chạy; nếu cần contract gốc, mở spec/code được dẫn ở cuối case.
"Notification mới tạo job nhưng không gửi ra"
Thường do Notification Settings ở bước Scheduler. Kiểm tra theo thứ tự:
- Loại notification đã seed vào
NotificationOptioncho BU chưa? (loại mới phải seed trước khi gửi được.) - BU có subscriber/recipient không? Không có → handler skip gracefully.
- Generator có handler cho loại mới chưa? Feature mới có thể chưa wire route.
Ví dụ thực tế: Bật loại notification mới cho BU, job tạo ra nhưng khách không nhận. → Nghẽn ở Scheduler/Generator. → Loại mới chưa seed vào
NotificationOptioncho BU → seed trước mới gửi được.
"UserMessage đã tạo nhưng Inbox chưa cập nhật ngay"
Realtime Inbox chỉ là tín hiệu invalidation, nên kiểm tra theo chuỗi:
UserMessageđã persist cho đúng recipient owner chưa?- Persistence outcome có phải tạo mới không? Re-ensure dòng tương thích không phát event mới.
- Publisher có ghi
inbox.changedvào Redis Stream theo đúng environment không? - Tuke có nhận event và gọi lại
UnreadCountkhông? Nếu Inbox đang mở, first page cũng phải revalidate.
Nếu Redis/SSE lỗi, đây không phải mất dữ liệu: REST, focus/visibility refresh và polling tiếp tục là đường convergence. Không gửi full message qua SSE và không dùng event để cấp quyền đọc.
Ví dụ thực tế: Generator tạo row
UserMessagethành công nhưng Redis publish lỗi. → Không rollback row và không báo event giả. → Badge sẽ hội tụ ở lần REST refresh/polling tiếp theo.
"SSE Inbox bị mất kết nối"
- Client reconnect với exponential backoff và gửi
Last-Event-IDgần nhất. - Event còn trong retention window được replay và client dedupe theo
eventId. - Event hết retention hoặc replay không hợp lệ trả
inbox.sync-required; client gọi lại unread và first page bằng REST. - Reconnect không được chặn việc mở, đọc hoặc mutate Inbox qua REST.
Ví dụ thực tế: Tuke mất mạng 2 phút rồi kết nối lại. → Event còn trong Redis được replay; nếu đã hết retention thì nhận
inbox.sync-required. → Inbox vẫn lấy trạng thái đúng bằng REST, không cần SignalR.
"Tra một UserMessage đang lưu ở đâu?"
Kiểm tra theo đúng thứ tự, không scan toàn bộ storage:
- Xác định portal identity và owner scope được phép:
Account_{id},Employee_{id}hoặcEnterpriseUser_{id}. - Point-read Azure Table
UserMessagebằngPartitionKey+RowKey; dữ liệu cũ mới thử thêmCustomer_{UserId}để compatibility-read. - Nếu cần nội dung, lấy
MessageBodyBlobNametừ row rồi đọc Blob containermessage-contentsau authorization; list chỉ cóMessagePreview. - Nếu cần badge, đọc counter
_InboxUnreadhoặc_InboxUnread_Account_{id}trong cùng partition; không dùng Redis Stream để tính unread.
NextCursor là cursor opaque do API cấp, chứa continuation state theo từng scope. Khi truy vấn trang tiếp theo phải gửi nguyên chuỗi cursor cùng bộ filter; không decode hoặc tự ghép RowKey ở client.
Ví dụ thực tế: Support có
EmployeeId=42vàRowKeytừ response Inbox. → ĐọcUserMessage/Employee_42/{RowKey}rồi lấy body theoMessageBodyBlobName. → Không queryCustomer_{UserId}hay dùng event Redis để suy ra nội dung.
Thêm placeholder vào template
Template dùng {{...}}. Ví dụ booking-notification-checkout-instruction thêm placeholder hướng dẫn checkout. Quy trình:
- Thêm placeholder vào template.
- Đảm bảo handler ở Generator cung cấp giá trị cho placeholder đó khi render.
- Placeholder thiếu dữ liệu → render rỗng (kiểm tra fallback).
Ví dụ thực:
{{INCIDENT_DETAILS_LINK}}trỏ tới trang/attendance/incident-management(chưa có deep-link tới một incident cụ thể — đổi sang deep-link khi route tồn tại).
Tắt/giảm một loại thông báo
Một số tình huống cần suppress (nén) notification:
| Case | Cách |
|---|---|
| Không gửi notification cho booking đang pending | Mute ở điều kiện pending |
| Tắt notification booking theo cấu hình subscription | Setting ở subscription |
Mute/suppress là notification-only: không đổi state booking/enrollment/invoice (xem nguyên tắc "alert mute is notification-only" bên Subscription).
Cấu hình recipient mặc định cho site
- Seed email người nhận mặc định cho site.
- Tùy chọn "Send copy to site contact email" chỉ hiển thị cho option site-level (
NotificationOption.LevelTypeId = Org); levelBusinessUnit/Enterprise(vd "Business Unit Setting updated") → option ẩn trong editor, không bao giờ persistSendToSiteContactEmail = true(request gửi true vẫn lưu false; channel flag, template, subscriber, sender giữ nguyên). - Khi sinh notification site-level: setting bật +
OrgSettings.EmailReplyTokhông rỗng → generator thêm site contact email làm recipient (không thay thế subscriber); tắt/email rỗng → không thêm. Áp dụng cho mọi handler site-level dựng recipient/subscriber và cả đường gửi trực tiếp (không qua generator). - Scope ưu tiên hơn stored flag ở thời điểm gửi: non-site-level không bao giờ nhận copy dù flag còn
true; không cần migration. - Handler account-facing (1 recipient duy nhất — booking/quote/subscription,
CancelBooking,Invoice, NCS attendance) chỉ dùng site contact như template variable, ngoài rule copy này.
Ví dụ thực tế: Admin bật copy cho
IncidentSubmitted(site-level), để setting bật ở option BU "Business Unit Setting updated". → Generator sinh notification. → BảnIncidentSubmittedthêm site contact email; bản BU không copy vì scope ưu tiên.
"Khách nhận email trùng" (phía gửi)
Khác với trùng ở Platform (tạo job trùng), ở đây là trùng lúc gửi. Phòng bằng send claim:
Created/RetryReady -> Sending (cập nhật có điều kiện)Nếu vẫn trùng, kiểm tra (1) OutgoingMessage đúng "một dòng/người nhận/kênh" không (RecipientKeyHash ổn định?), (2) send claim có điều kiện theo Status không, (3) AttemptNo có bị thêm ngoài ý muốn không.
Ví dụ thực tế: Phụ huynh nhận 2 email xác nhận booking giống hệt. → Mỗi người/kênh gửi một lần. → Send claim chuyển
Created → Sendingcó điều kiện theo Status → worker thứ hai không claim lại được.
Campaign gửi tới nhiều người: payload lớn
Dùng MessagingJob + Blob. Lưu ý:
- Nội dung lớn để ở Blob (
messaging-jobcontainer), SQL chỉ giữ điều phối + tên blob. - Retry tái dùng blob payload tương thích; payload lệch → dừng + báo xung đột (không ghi đè).
Báo cáo định kỳ (report subscription) cutover an toàn
Report subscription là một đường mỏng có:
- Shadow mode + allowlist + one-owner guard để scheduler cũ và mới không bao giờ cùng gửi một kỳ báo cáo.
- Cutover từng bước, rollback được. Chi tiết: Report Subscription.
"Việc bị skip/deactivate — tại sao không gửi?"
Khi automation bị skip/deactivate, không có delivery. Lý do nằm ở AutomationScheduler.DispatchResultJson (SkippedReasonCode/ConflictReasonCode/Summary) và các màn hình support — không phải ở pipeline gửi.
Ví dụ thực tế: Support được hỏi vì sao reminder của một lịch không gửi. → Tìm đúng nơi giải thích thay vì lục pipeline. →
DispatchResultJsonhiệnSkippedReasonCode→ skip có chủ đích, không phải lỗi gửi.
"Không tìm thấy lịch sử notification theo invoice/account"
Các flow đã wire dùng source-keyed OutgoingMessage (invoice:{guid}, account:{guid}, ...). Tra theo partition source và kiểm tra source đã resolve; booking cart có thể giữ OrderGuids để lọc trong account partition. Không scan toàn Table và không đổi key lịch sử cũ.
Ví dụ thực tế: Một job chứa 3 invoice nhưng support cần invoice thứ hai. → Không mở history theo job GUID chung. → Tìm
invoice:{invoice.Guid}của invoice thứ hai và xem từng recipient/channel.
"BU contact nhận email trùng Site contact"
Kiểm tra hai copy flag và recipient normalization. Delivery phải dedupe địa chỉ sau khi resolve; nếu notification không phải site-level thì Site copy bị bỏ qua theo scope.
Ví dụ thực tế: Site contact và BU contact cùng
ops@example.com. → Chỉ một email được tạo. → Dedupe recipient trướcOutgoingMessage, không sửa subscriber gốc.
"Report Subscription chạy nhưng không có file đính kèm"
Đây có thể là behavior đúng: mọi run lưu file và cung cấp secure link; attachment chỉ là tùy chọn, file quá lớn sẽ chuyển link-only. Kiểm tra run history, attachment/link state và delivery per recipient.
Ví dụ thực tế: Roll Sheet 25 MB vượt giới hạn mail. → Không làm mất report. → History ghi link-only, email dẫn tới view/download đã authorize.