Communication — Kiến trúc hệ thống
Ranh giới rõ ràng: phần logic (Platform/automation) quyết định gửi gì; phần dispatch đảm bảo bản ghi tồn tại; pipeline gửi lo gửi thật. Handler automation không bao giờ gọi provider trực tiếp.
Toàn cảnh
Một câu: event và lịch từ bên trái dồn vào
NotificationScheduler; pipeline worker (Scheduler → Generator → Distributor) render và gửi; webhook cập nhật trạng thái vềOutgoingMessage.
Chọn đường gửi theo producer
| Producer | Đường |
|---|---|
| Manual / tin entity tức thời | NotificationJob → NotificationScheduler |
| Direct reminder | NotificationJob → NotificationScheduler → OutgoingMessage |
| UI-only reminder | NotificationJob → NotificationScheduler → UserMessage |
| Report subscription | AutomationScheduler → MessagingJob+Blob và/hoặc NotificationJob → NotificationScheduler → OutgoingMessage |
| Messaging campaign | MessagingJob+Blob → NotificationScheduler → OutgoingMessage |
| Automation bị skip/deactivate | Không gửi; kết quả ở AutomationScheduler.DispatchResultJson + màn hình support |
Ensure contracts (IDeliveryDispatcher)
Mọi artifact được tạo qua một hợp đồng Ensure*:
IDeliveryDispatcher
EnsureAutomationJobAsync EnsureNotificationJobAsync
EnsureMessagingJobAsync EnsureUserMessageAsync
EnsureOutgoingMessageAsync EnsureNotificationSchedulerAsyncMỗi method: tìm theo khóa tất định → kiểm tra tương thích (InputHash/ContentHash + immutable fields) → tạo nếu thiếu → trả về tọa độ ổn định. Tương thích → idempotent success; lệch immutable → xung đột (dừng gửi, báo support).
Key policy (rút gọn)
Khóa quyết định "mở lịch sử ở đâu" và "chống trùng thế nào".
NotificationJob
Manual / transactional: PartitionKey = PrimaryEntity.Guid
RowKey = {CreatedReverseTicks}_{NotificationJobGuid}
Direct automation reminder: PartitionKey = PrimaryEntity.Guid
RowKey = AutomationRowKey
Report subscription: PartitionKey = ReportSubscription.Guid
RowKey = AutomationRowKey
Messaging campaign: PartitionKey = {BusinessUnit.Guid}_Campaign
RowKey = Campaign.GuidPartitionKey là entity nơi người dùng mong đợi mở lịch sử — không phải id nội bộ của automation. RowKey = AutomationRowKey là cầu nối idempotency từ automation sang notification. Notification event-derived dùng NotificationEventKey (xem Platform).
UserMessage
Customer: PartitionKey = user:{User.Id}
Staff: PartitionKey = employee:{Employee.Guid}
RowKey = {SourceReverseTicks}_{NotificationJob.RowKey}Prefix user: / employee: giữ hai miền danh tính tách biệt. SourceReverseTicks phải lấy từ timestamp nguồn ổn định (không phải write-time) để Generator retry không tạo dòng trùng.
OutgoingMessage + send claim
Notification-generated: PartitionKey = NotificationJob.RowKey
Report/campaign: PartitionKey = MessagingJob.Guid
RowKey = {RecipientKeyHash}_{DeliveryMethod} (+ _{AttemptNo} chỉ khi cố ý là attempt mới)RecipientKeyHash dẫn xuất từ danh tính người nhận ổn định + đích đã chuẩn hóa, không phải text hiển thị. Một dòng / người nhận / kênh; Generator retry tái dùng dòng.
Claim trước khi gọi provider:
Created/RetryReady -> Sending WHERE PartitionKey, RowKey, Status in (Created, RetryReady)Dòng đã Sending | SentToProvider | Delivered | Opened | FailedFinal → tin trùng trong queue thoát mà không gọi provider. Provider thành công → SentToProvider + provider message id; webhook đẩy tiếp. Chỉ một support-retry có audit mới reset về RetryReady.
NotificationScheduler dispatch guard
UNIQUE(ArtifactTypeId, ArtifactPartitionKey, ArtifactRowKey, NotificationEventTypeId)EnsureNotificationSchedulerAsync trả dòng có sẵn khi guard khớp. NotificationEventTypeId là discriminator của domain notification — map tường minh ở biên notification, không giả định bằng EventTypeId của EntityEvent.
Xử lý lỗi từng phần (partial failure)
| Tìm thấy khi retry | Hành vi |
|---|---|
NotificationJob có, scheduler thiếu | Tạo lại scheduler qua dispatch guard |
UserMessage thiếu dòng người nhận | Chèn dòng thiếu với khóa tất định |
OutgoingMessage đã có | Tái dùng; provider call do send claim quyết định |
MessagingJob có, blob không tương thích | Dừng, báo xung đột |
| Bất kỳ khóa nào hash không khớp | Dừng, báo xung đột |
Tiếp theo: file & worker cụ thể ở Luồng xử lý trong code.