notify

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 6, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

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

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 Channel

type Channel string

Channel is a notification delivery channel.

const (
	ChannelInApp    Channel = "inapp"
	ChannelEmail    Channel = "email"
	ChannelSMS      Channel = "sms"
	ChannelWhatsApp Channel = "whatsapp"
	ChannelPush     Channel = "push"
)

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.

func (*FakeSender) Send

func (f *FakeSender) Send(_ context.Context, d Delivery) (string, error)

Send records the delivery and returns a fake provider message ID, or 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 NewRegistry

func NewRegistry() *Registry

NewRegistry returns an empty template registry.

func (*Registry) Err

func (r *Registry) Err() error

Err returns accumulated registration errors joined, or nil.

func (*Registry) Get

func (r *Registry) Get(key string) (TemplateSpec, bool)

Get returns the spec for a key.

func (*Registry) Keys

func (r *Registry) Keys() []string

Keys returns registered keys, sorted.

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 New

func New(reg *Registry, idgen model.IDGen, opts ...Option) *Service

New wires the service. reg and idgen are required.

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

func (s *Service) Send(ctx context.Context, db database.TenantDB, msg Message) (uuid.UUID, error)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL