Platform — Codebase flow
This page is for engineers: it maps architecture to specific files/classes in Tux (.NET backend + workers). Paths are relative to repo root.
Components & files
| Component | File (approx.) | Role |
|---|---|---|
NotificationJobFactory | Tux.Core/Services/NotificationJobFactory.cs | Single job-creation boundary: serialize EventData → string exactly once, set StatusId, init Recipients. |
NotificationEventKey | Tux.Core/Services/NotificationEventKey.cs | Creates deterministic EventKey for worker-created jobs (idempotency). |
IEntityEventNotificationMapping | Workers/Tux.Workers.EventHandler/Notifications/ | 1 domain = 1 mapping; returns job or no-match. |
EntityEventNotificationPostProcessor | Workers/Tux.Workers.EventHandler/Notifications/ | Walks every notification mapping; owns dedup + job + scheduler write. |
HintedNotificationMapping | nt | Reads NotificationEventTypeId hint, creates id-only job. |
SettingsUpdateNotificationMapping | nt | Sample mapping for settings-update (event-derived). |
NotificationClient | Services/Tux.Service/Clients/NotificationClient.cs | Persists job + creates scheduler (direct path). |
EntityEventHistoryPostProcessor | Workers/Tux.Workers.EventHandler/... | Walks history registry; owns deterministic RowKey + persistence. |
IEntityEventHistoryMapping / ...MultiMapping | nt | Domain rule: match + build projection (single / one-to-many). |
HistoryEventKey | Tux.Core/Services/... | Deterministic RowKey for history row (mirrors NotificationEventKey). |
| Send pipeline | Tux.Workers.Notification.{Scheduler,Generator,Distributor} | Scheduler → Generator → Distributor. |
The paths above are anchors for quick
rg/grep lookup. If structure changes, search again by class name.
Flow A — Event-derived (via event)
Publishing service:
// Chỉ publish event như cũ, kèm hint tại đúng nhánh cần notify
await SendAttendeeEventAsync(actionTypeId, attendee, timestamp,
notificationEventTypeId: (int)NotificationEventType.AttendeeNewProfile);
// (đã bỏ direct CreateNotificationJobAsync)Worker mapping (shortened):
public sealed class SettingsUpdateNotificationMapping : IEntityEventNotificationMapping
{
public bool TryMap(EntityEventPostProcessingContext ctx, out NotificationJobModel job)
{
// match theo event identity + (nếu cần) explicit hint
job = NotificationJobFactory.Create(
eventTypeId: NotificationEventType.SettingsUpdate,
businessUnitId: buId,
eventKey: NotificationEventKey.ForEntityEvent(...), // deterministic ⇒ idempotent
eventData: new NotificationSettingUpdateModel { /* ... */ });
return true;
}
}Two current hint styles
EntityEventMessage.NotificationEventTypeId— typed field, used for id-only flow (EventDatais only entity id). Clean, read byHintedNotificationMapping.notificationSettingUpdateTypeIdembedded in JSONPayload— parsed bySettingsUpdateNotificationMapping.TryReadHint(must guard against duplicate numeric ids).
Planned (not done): merge (2) into typed field so there is only one hint concept.
Flow B — Direct (action-derived)
var job = NotificationJobFactory.Create(
eventTypeId: NotificationEventType.AttendeeProfileUpdate,
eventKey: Guid.NewGuid().ToString(), // at-least-once cho flow không có identity ổn định
eventData: new { AttendeeId = id, SummaryChange = htmlDiff }); // context tính tại call-time
await _notificationClient.CreateNotificationJobAsync(_context, job);NotificationClient.CreateNotificationJobAsync sets StatusId, resolves BusinessUnitId from context, writes NotificationJob + creates NotificationScheduler.
Important id-only contract
For hinted flows, EventData is always entity id (integer serialized as string). The matching Generator handler reads it with:
JsonConvert.DeserializeObject<int>((string)job.EventData)(see HandleAccountNewRegistration_EmployeeAsync, HandleStaffProfileUpdateAsync, HandleIncidentSubmittedAsync).
⚠️ Do not change id-only shape to object
{ ... }— it will break Generator.
Test guardrails
| Test | Protects |
|---|---|
NotificationDirectCreationGuardrailTests | Pins every remaining direct CreateNotificationJobAsync call (with count) into an allowlist with notes. New direct call → red test, forcing "should this be event-derived?" discussion. |
| dual-path guardrail test | Event-derived (hinted) type set and direct type set must be disjoint; overlap → fail (prevents double send). |
EventData round-trip test | Locks that saved EventData is always a parseable string (prevents (string)EventData regression). |
Error policy & idempotency (decided)
- Event-derived (worker): job creation error →
throwso queue retries (safe because deterministic key ⇒ idempotent). - Action-derived (direct, inside business transaction): best-effort — notification error only logs and does not break the business transaction.
Flows migrated to event-derived
| Flow | Mechanism | Event publish | Call-site |
|---|---|---|---|
AccountNewRegistration | hint (id-only) | Create_Account | AccountService.CreateAccountAsync (registration branch) |
StaffProfileUpdate | hint (id-only) | Update_Employee (branch isEdit) | EmployeeService.UpsertEmployeeAsync |
IncidentSubmitted | hint (id-only) | Submit_Incident (1750→FormInstance) | IncidentManagementService.SubmitAsync |
Finance_Account_Codes_Settings | SettingsUpdate mapping | Update_BusinessUnit_AccountCode_Setting (706) | AccountCodeService.UpdateAccountCodeAsync |
Notification_Settings | SettingsUpdate mapping | Update_BusinessUnit_Notification_Setting (707) | NotificationSettingService (3 call-sites) |
Flows still direct are action-derived (AccountProfileUpdate, AttendeeProfileUpdate, booking/quote, invoice/statement, direct-payment, subsidy, employee-absence, etc.) — intentionally kept direct.
Next: practical situations in Common cases.