Alert
Source: src/data/alert/
Service
createAdminNotificationService / AdminNotificationService
Factory: createAdminNotificationService() — returns AdminNotificationService.
Persists tenant-scoped admin notifications for order and payment lifecycle events.
| Method | Parameters | Returns | Description |
|---|---|---|---|
| notify | (ctx: RequestContext, input: { type: AdminNotificationEventType, message: string, entityType: string, entityId: string }) | Promise<void> | Persists a tenant-scoped admin notification event for operational follow-up |
AdminNotificationEventType is one of ORDER_SUBMITTED, PAYMENT_CONFIRMED, PAYMENT_EXCEPTION.
The service maps notification types to alert types:
ORDER_SUBMITTED→NEW_ORDERPAYMENT_CONFIRMED→NEW_ORDERPAYMENT_EXCEPTION→PAYMENT_FAILED
Repository
AlertRepository
| Method | Parameters | Returns |
|---|---|---|
| getById | (ctx: RequestContext, id: string) | Promise<AlertRecord | null> |
| listByTenant | (ctx: RequestContext) | Promise<AlertRecord[]> |
| create | (ctx: RequestContext, record: AlertRecord) | Promise<void> |
| update | (ctx: RequestContext, id: string, patch: Partial<Omit<AlertRecord, 'id' | 'tenantId'>>) | Promise<AlertRecord> |
| delete | (ctx: RequestContext, id: string) | Promise<void> |
AlertRecord
| Field | Type |
|---|---|
| id | string |
| tenantId | string |
| createdAt | Date |
| updatedAt | Date |
| data | Record<string, unknown> (optional) |
Validators
alertTypeSchema
Enum: LOW_INVENTORY, NEW_ORDER, PAYMENT_FAILED, FULFILLMENT_FAILED
alertSeveritySchema
Enum: INFO, WARN, ERROR
alertSchema
| Field | Type | Required | Default |
|---|---|---|---|
| tenantId | string | Yes | — |
| type | alertTypeSchema | Yes | — |
| severity | alertSeveritySchema | Yes | — |
| message | string | Yes | — |
| entityType | string | No | — |
| entityId | string | No | — |
| isRead | boolean | No | false |
| createdAt | Date (coerced) | Yes | — |
| updatedAt | Date (coerced) | Yes | — |
Inferred Types
Alert—z.infer<typeof alertSchema>AlertType—z.infer<typeof alertTypeSchema>AlertSeverity—z.infer<typeof alertSeveritySchema>
NotificationService
Source: src/data/alert/notification.service.ts
Factory: createNotificationService({ cache }) — returns NotificationService.
Sends Discord messages to admin channels and customer DMs. Deduplicates per eventId + consumerId using Valkey (24 h TTL). Retries once on Discord 429 responses (honouring Retry-After).
| Method | Parameters | Returns | Description |
|---|---|---|---|
| notifyAdminChannel | (ctx, tenantId, payload, opts) | Promise<NotifyResult> | Posts to tenant's announcement/admin channel |
| notifyCustomerDm | (ctx, discordUserId, payload, opts) | Promise<NotifyResult> | DMs a Discord user (create DM channel → send) |
NotifyResult — { skipped: boolean; messageId?: string }
NotificationPayload — { content?: string; embeds?: DiscordEmbed[]; components?: DiscordActionRow[] }
NotifyOptions — { eventId: string; consumerId: string; botToken: string; channelId?: string }
Notification Consumers
Source: src/data/alert/notification.consumers.ts
Registers event handlers on the app event bus. Called from src/registries/events.registry.ts.
| Event | Consumer ID | Target |
|---|---|---|
order.created | order.created:adminChannel | Admin channel — approval embed + buttons |
order.created | order.created:customerDm | Customer DM — order received + payment instructions |
payment.rejected | payment.rejected:adminChannel | Admin channel — rejection audit post |
payment.confirmed | payment.confirmed:customerDm | Customer DM — itemized receipt embed (items, subtotal, discount, total, payment method, ETA) |
order.shipped | order.shipped:customerDm | Customer DM — tracking or pickup notice |
order.refunded | order.refunded:customerDm | Customer DM — refund notice + reason |
DM preferences: each customer DM is gated on the matching customer.dmPreferences.<key> flag. If the flag is false, the DM is skipped and an audit entry is written.
Email notifications: after each customer DM, if emailPort is wired, customer.email is on file, and the corresponding customer.emailPreferences.<key> flag is true, a transactional email is sent via EmailPort. Email failures are non-blocking — they are swallowed with .catch(() => undefined) so a send failure never prevents the DM from being delivered. Email templates:
| Event | Template file |
|---|---|
order.created | src/data/alert/email-templates/order-received.template.ts |
payment.confirmed | src/data/alert/email-templates/payment-confirmed.template.ts |
order.shipped | src/data/alert/email-templates/order-shipped.template.ts |
order.refunded | src/data/alert/email-templates/order-refunded.template.ts |
DM opt-out: if customer.dmOptOut === true, customer DMs are skipped and an audit log entry (notification.dm_skipped.opt_out) is written instead.
inventory.low is intentionally NOT handled here — that alert is owned by the Inventory feature and fires a separate @everyone mention.
EmailPort
Source: src/types/email.types.ts, src/ports/email.port.ts, src/adapters/sendgrid-email.adapter.ts
Port contract for transactional email delivery. Two implementations:
| Implementation | Factory | Condition |
|---|---|---|
| SendGrid (HTTPS v3 API) | createSendGridEmailAdapter({ apiKey, from }) | EMAIL_PROVIDER=sendgrid + SENDGRID_API_KEY + EMAIL_FROM set |
| No-op | createNoopEmailPort() | Default when SendGrid is not configured |
EmailPort interface:
| Method | Parameters | Returns |
|---|---|---|
send | EmailSendInput | Promise<EmailSendResult> |
EmailSendInput:
| Field | Type | Required |
|---|---|---|
to | string | Yes |
subject | string | Yes |
html | string | Yes |
text | string | Yes |
tags | string[] | No |
EmailSendResult: { accepted: boolean; messageId?: string }
The SendGrid adapter uses the native Node.js https module (no SDK dependency). It throws on 4xx responses (permanent delivery failure) and on 5xx responses (transient; caller may retry via outbox). The from address is sourced from the EMAIL_FROM environment variable.