Documentation
¶
Overview ¶
Package notify is wowapi's notification framework: modules register template keys with an allowlisted variable set and required channels (Registry); Send writes a notifications row + one notification_deliveries row per resolved channel inside the caller's tenant business transaction (atomicity with the business write); and SendPending is the async worker step that claims queued deliveries, calls channel-specific senders, and advances delivery status — dead-lettering after maxAttempts. In-app deliveries are rows queried by a future /notifications API. Contract: blueprint 07 §5.
Index ¶
- func RenderBody(spec TemplateSpec, channel Channel, body string, vars map[string]any) (string, error)
- func ValidateBody(spec TemplateSpec, body string) error
- type Channel
- type ChannelDest
- type ChannelSender
- type Delivery
- type DeliveryReceipt
- type FakeSender
- type Importance
- type Message
- type Notification
- type Option
- type Registry
- type Service
- func (s *Service) Deliveries(ctx context.Context, db database.TenantDB, notificationID uuid.UUID) ([]DeliveryReceipt, error)
- func (s *Service) ListForParty(ctx context.Context, db database.TenantDB, partyID uuid.UUID) ([]Notification, error)
- func (s *Service) RegisterSender(channel Channel, sender ChannelSender)
- func (s *Service) Send(ctx context.Context, db database.TenantDB, msg Message) (uuid.UUID, error)
- func (s *Service) SendPending(ctx context.Context, plat database.TxManager, tenantID uuid.UUID, ...) (int, error)
- func (s *Service) SetChannelPref(ctx context.Context, db database.TenantDB, partyID uuid.UUID, channel Channel, ...) error
- type TemplateSpec
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func RenderBody ¶
func RenderBody(spec TemplateSpec, channel Channel, body string, vars map[string]any) (string, error)
RenderBody is the exported version of renderBody for use by ChannelSender adapters (e.g. smtp, sms) that need to render a body fetched from the DB before dispatching. channel selects the escaping context (see renderBody): email → html/template (auto-escaped), everything else → text/template.
func ValidateBody ¶
func ValidateBody(spec TemplateSpec, body string) error
ValidateBody parses the template body and verifies every top-level field reference ({{.VarName}}) is declared in spec.Vars. Call this at seed time when writing a template row to the database — blueprint 07 §5: "must fail at REGISTER/seed-validation time (not at send time)". Returns KindValidation on violation.
Types ¶
type ChannelDest ¶
type ChannelDest struct {
Channel Channel
Destination string // empty = auto-set to partyID for inapp; required for others
}
ChannelDest is a channel + destination pair in a Send request.
Channel resolution simplification (SEC decision): party_contacts.kind ('email','phone','address','other') does not map cleanly to notification channels ('inapp','email','sms','whatsapp','push'). Rather than invent a domain contact schema, Message accepts explicit ChannelDest pairs. The in-app channel needs no external destination — it defaults to the recipient party ID string when Destination is empty.
type ChannelSender ¶
type ChannelSender interface {
// Send attempts to deliver d. It returns the provider-assigned message ID
// on success, or an error. Errors are recorded on the delivery row and
// retried up to maxAttempts.
Send(ctx context.Context, d Delivery) (providerMessageID string, err error)
}
ChannelSender is the port for channel-specific delivery adapters (smtp, sms, whatsapp, push). Real adapters implement this interface; the fake is provided for tests. Adapters are responsible for fetching and rendering the template body (via RenderBody) from the notification_templates DB rows when needed — the Delivery carries routing information only, not rendered content.
type Delivery ¶
type Delivery struct {
ID uuid.UUID
TenantID uuid.UUID
NotificationID uuid.UUID
Channel Channel
Destination string
Status string
Attempts int
ProviderMsgID string
LastError string
}
Delivery is a persisted notification_deliveries row, passed to ChannelSender on each send attempt. It carries routing information (channel, destination) only; real adapters must load and render the template body themselves via RenderBody if needed.
type DeliveryReceipt ¶
type DeliveryReceipt struct {
ID uuid.UUID
Channel Channel
Destination string
Status string // queued | sent | delivered | failed | dead
Attempts int
ProviderMsgID string
LastError string
CreatedAt time.Time
UpdatedAt *time.Time
}
DeliveryReceipt is the per-channel delivery record for a notification: its status, attempt count, the provider's message id (receipt), and the last error. It answers "did this notification actually go out, on which channels, and what did the provider say" (roadmap R5).
type FakeSender ¶
type FakeSender struct {
Deliveries []Delivery
Err error // if non-nil, returned by every Send call
// contains filtered or unexported fields
}
FakeSender is an in-memory ChannelSender for integration tests. It records every Delivery passed to Send and returns a deterministic provider message ID ("fake-msg-" + delivery ID). Set Err to make Send return an error.
func (*FakeSender) Count ¶
func (f *FakeSender) Count() int
Count returns the number of deliveries recorded (thread-safe).
func (*FakeSender) Reset ¶
func (f *FakeSender) Reset()
Reset clears recorded deliveries and the configured Err.
type Importance ¶
type Importance string
Importance ranks how critical a notification is.
const ( ImportanceNormal Importance = "normal" ImportanceImportant Importance = "important" // ImportanceLegal requires an audit trail on delivery (blueprint 07 §5). ImportanceLegal Importance = "legal" )
type Message ¶
type Message struct {
TemplateKey string
RecipientPartyID uuid.UUID
// Variables is the variable map rendered into the template body by
// async senders. All keys must be in the registered TemplateSpec.Vars
// allowlist; unknown keys are rejected with KindValidation.
Variables map[string]any
Channels []ChannelDest
Importance Importance
Resource resource.Ref
// Locale is the desired locale for template lookup; empty defaults to "en".
// Template resolution follows the fallback chain: e.g. "hi-IN" → "hi" → "en".
Locale string
}
Message is the input to Service.Send.
type Notification ¶
type Notification struct {
ID uuid.UUID
TenantID uuid.UUID
TemplateKey string
RecipientPartyID uuid.UUID
Variables map[string]any
ResourceType *string
ResourceID *uuid.UUID
Importance Importance
Status string
CreatedAt time.Time
CreatedBy uuid.UUID
}
Notification is a persisted notification row returned by ListForParty.
type Option ¶
type Option func(*Service)
Option customizes the notify Service.
func WithTracer ¶
func WithTracer(tr observability.Tracer) Option
WithTracer wires a tracer so each queued delivery captures the current request's W3C traceparent (roadmap O1/CA-9) into the delivery envelope; the async sender (SendPending) continues that trace when it delivers. Default: NoOpTracer (empty trace context — no behavior change). Mirrors the outbox writer/relay tracer seam.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry holds TemplateSpec declarations made by modules at boot. Keys must be module.area.name and a module may only register keys with its own prefix.
func (*Registry) Get ¶
func (r *Registry) Get(key string) (TemplateSpec, bool)
Get returns the spec for a key.
func (*Registry) Register ¶
func (r *Registry) Register(module string, spec TemplateSpec)
Register records a template key's spec. Errors (bad key, prefix mismatch, duplicate) accumulate and are returned by Err().
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service is the notification framework. Module-facing operations (Send, ListForParty) run inside the caller's tenant transaction (app_rt). Platform operations (SendPending) run under a tenant-bound TxManager (app_platform) to advance append-only delivery status — the same split as kernel/document.
func (*Service) Deliveries ¶
func (s *Service) Deliveries(ctx context.Context, db database.TenantDB, notificationID uuid.UUID) ([]DeliveryReceipt, error)
Deliveries returns the delivery receipts for a notification, one per channel fan-out, newest state first-written order. Runs in the caller's tenant tx (RLS-scoped), so a caller only ever sees its own tenant's receipts.
func (*Service) ListForParty ¶
func (s *Service) ListForParty(ctx context.Context, db database.TenantDB, partyID uuid.UUID) ([]Notification, error)
ListForParty returns the notifications for a party (the in-app inbox), newest first. Runs on the caller's read-only or read-write tenant tx.
func (*Service) RegisterSender ¶
func (s *Service) RegisterSender(channel Channel, sender ChannelSender)
RegisterSender registers a ChannelSender for the given channel. Call this at wiring time for each transport (email, sms, whatsapp, push). An in-app sender is registered by default.
func (*Service) Send ¶
Send writes one notifications row and one notification_deliveries row per resolved channel, all within the caller's tenant transaction. Returns the notification id. Errors if the template key is not registered, any variable key is outside the spec's allowlist, or no template exists in the DB for any requested channel.
func (*Service) SendPending ¶
func (s *Service) SendPending(ctx context.Context, plat database.TxManager, tenantID uuid.UUID, now time.Time) (int, error)
SendPending is the async worker step. It runs as app_platform (tenant-bound): claims queued notification_deliveries with FOR UPDATE SKIP LOCKED, calls the registered ChannelSender for each, and advances status:
- success → 'sent' (provider_message_id set)
- failure → 'failed' (attempts incremented, next_attempt_at = now + backoff(newAttempts)); at maxAttempts → 'dead'
ARCH-75: a 'failed' delivery is re-claimed only once its next_attempt_at has elapsed (relative to the passed `now`), so a transient outage does not burn all maxAttempts in seconds and permanently dead-letter. The backoff schedule is monotonic (see backoff).
NOTE: claim + sender call + status update happen in one transaction. Real production deployments should move the network call outside the tx to avoid holding locks during I/O; the fake sender in tests is synchronous so this is safe for the test suite.
NOTE: ImportanceLegal deliveries would additionally write an audit trail, but app_platform lacks INSERT on events_outbox (see migration 00007). Legal delivery auditing is deferred to a future audit_logs writer.
Returns the number of deliveries successfully sent.
func (*Service) SetChannelPref ¶
func (s *Service) SetChannelPref(ctx context.Context, db database.TenantDB, partyID uuid.UUID, channel Channel, enabled bool) error
SetChannelPref records a recipient's opt-in/opt-out for a channel (R5). Absence of a preference means enabled, so this is only needed to opt OUT (or to re-enable after opting out). Runs in the caller's tenant tx.
type TemplateSpec ¶
type TemplateSpec struct {
Key string
Vars []string // allowlisted variable names — {{.Name}} needs "Name" here
Channels []string // expected channel names (informational; guides seeding)
}
TemplateSpec is the static declaration a module makes for a template key. Vars lists every variable name the template body is permitted to reference; the body must not reference any name outside this set (ValidateBody enforces this at seed time). Channels lists channels the module expects templates for.