Communication Preferences Module
Manages per-entity communication channel preferences. Controls which channels (email, SMS, WhatsApp, Messenger) and communication types (invoices, payment reminders, etc.) an entity has opted into or out of.
Domain Concepts
- Entity: Any identifiable record (customer, user, business_user) identified by
(entityType, entityId, businessId). - Channel toggles: Global on/off flags per channel (
emailEnabled,smsEnabled,whatsappEnabled,messengerEnabled). - Type-specific preferences: A JSON object mapping communication types to arrays of allowed channels (e.g.
{ "invoice": ["email"], "payment_reminder": ["email", "sms"] }). - Opt-out: A timestamp (
optedOutAt) + optional reason that blocks all communications regardless of channel flags. - Preferred contacts: Explicit contact details per channel (
preferredEmail,preferredPhone, etc.). - Fail-open default: If no preference record exists for an entity, all channels are considered enabled.
Architecture
Follows hexagonal (ports & adapters) architecture:
communication-preferences/
├── communication-preferences.module.ts
├── domain/
│ └── communication-preferences-repository.domain.ts # Port (interface + DI token)
├── application/
│ └── communication-preferences.service.ts # Use cases
├── infrastructure/
│ └── communication-preferences.repository.ts # Kysely adapter
└── interfaces/
├── communication-preferences.controller.ts # HTTP adapter
├── dtos/
│ ├── create-preference.dto.ts
│ └── update-preference.dto.ts
└── query/
└── paginate-communication-preferences.query.ts
Dependency flow: Controller → Service → Repository (via DI token COMMUNICATION_PREFERENCES_REPOSITORY).
API Endpoints
| Method | Path | Description |
|---|---|---|
POST | /communication-preferences | Create a preference record |
GET | /communication-preferences | List all (paginated, requires businessId) |
GET | /communication-preferences/:entityType/:entityId?businessId= | Get by entity (returns defaults if none exist) |
PATCH | /communication-preferences/:entityType/:entityId?businessId= | Upsert preferences |
DELETE | /communication-preferences/:id | Delete by ID (HTTP 204) |
Key Use Case: canSend()
The service exposes canSend(entityType, entityId, businessId, channel, type?) used by other modules (communications queue, template engine) to check whether a message can be sent:
- If no preferences exist → allow (fail-open)
- If entity has opted out (
optedOutAtset) → block - If channel toggle is
false→ block - If
typeis provided and type-specific preferences exist → check ifchannelis in the allowed list - Otherwise → allow
Database
Table: communication_preference
| Column | Type | Description |
|---|---|---|
id | UUID (PK) | Auto-generated |
business_id | UUID (FK) | Scopes to business |
entity_type | varchar | e.g. "customer", "user" |
entity_id | UUID | FK to the entity |
email_enabled | boolean | Default true |
sms_enabled | boolean | Default true |
whatsapp_enabled | boolean | Default true |
messenger_enabled | boolean | Default true |
preferences | jsonb | Type → channels map |
preferred_email | varchar | Preferred email |
preferred_phone | varchar | Preferred phone |
preferred_whatsapp | varchar | Preferred WhatsApp |
preferred_messenger_id | varchar | Preferred Messenger ID |
opted_out_at | timestamp | When opted out |
opted_out_reason | varchar | Why opted out |
created_at | timestamp | Auto-set |
updated_at | timestamp | Set on update |
Composite lookup: (entity_type, entity_id, business_id).
Related Modules
- communications — sends actual messages; calls
canSend()before dispatch - communication-templates — manages message templates
- communication-queue — queues and retries messages
- business-communication-config — business-level config (per-type, per-channel)
Design Decisions
- Fail-open — missing preferences default to "all enabled" rather than blocking. This avoids silent communication failures for new entities.
- Upsert on PATCH — the
PATCH /:entityType/:entityIdendpoint creates a record if none exists, reducing friction for clients. - JSON preferences column — type-specific preferences are stored as JSONB rather than a separate table, keeping the schema simple for a sparse feature matrix.
- DI token injection — the repository is injected via
COMMUNICATION_PREFERENCES_REPOSITORYtoken so the service depends on the interface, not the implementation.