Recipient Rules Module
Overviewβ
The recipient-rules module manages rules that determine who receives business communications. Each rule maps a (communicationType, channel) pair to a targeting strategy, defining who should be notified when a specific communication is triggered.
Backend path: apps/backend/src/recipient-rules/
Architectureβ
Follows hexagonal architecture:
domain/
recipient-rules-repository.domain.ts # Port (interface)
application/
recipient-rules.service.ts # Use cases / business logic
infrastructure/
recipient-rules.repository.ts # Kysely adapter (implements port)
interfaces/
recipient-rules.controller.ts # HTTP controller
dtos/
create-recipient-rule.dto.ts # Create request DTO
update-recipient-rule.dto.ts # Update request DTO
validators/
rule.validator.ts # Custom class-validator decorators
__tests__/rule.validator.spec.ts # Unit tests
Dependencies:
RecipientGroupsModuleβ validates group existenceCommunicationsModule(forward ref) βRecipientResolverServicefor preview
Domain Conceptsβ
Targeting Typesβ
Each rule uses exactly one targeting strategy:
| Type | Field | Description |
|---|---|---|
role | roleName | Targets all business users with a specific role |
group | groupId | Targets all members of a recipient group |
ad_hoc_email | adHocEmail | Targets a specific email address |
ad_hoc_phone | adHocPhone | Targets a specific phone number (E.164) |
ad_hoc_whatsapp | adHocWhatsapp | Targets a specific WhatsApp number (E.164) |
Rules are mutually exclusive β only one targeting field can be set per rule.
Priorityβ
Rules have a priority field (lower = higher priority, default 0). Rules are returned ordered by priority ascending.
Lifecycleβ
Rules support both hard delete (DELETE /:id) and soft delete (POST /:id/deactivate). Soft-deleted rules have isActive: false and are excluded from recipient resolution.
API Endpointsβ
| Method | Path | Description |
|---|---|---|
POST | /recipient-rules | Create a new rule |
GET | /recipient-rules/business/:businessId | List all rules for a business |
GET | /recipient-rules/business/:businessId/preview | Preview resolved recipients |
GET | /recipient-rules/:id | Get a single rule |
PATCH | /recipient-rules/:id | Update rule (priority, isActive) |
DELETE | /recipient-rules/:id | Hard delete a rule |
POST | /recipient-rules/:id/deactivate | Soft delete (set isActive=false) |
Query Parametersβ
GET /business/:businessId:
communicationType(optional) β filter by type (e.g.low_stock_alert)channel(optional) β filter by channel (e.g.email,sms,whatsapp)
GET /business/:businessId/preview:
communicationType(required) β type to previewchannel(required) β channel to preview
Example: Create a Role-Based Ruleβ
POST /recipient-rules
{
"businessId": "uuid",
"createdBy": "uuid",
"communicationType": "low_stock_alert",
"channel": "email",
"targetingType": "role",
"roleName": "inventory_manager",
"priority": 1
}
Example: Create an Ad-Hoc Email Ruleβ
POST /recipient-rules
{
"businessId": "uuid",
"createdBy": "uuid",
"communicationType": "daily_report",
"channel": "email",
"targetingType": "ad_hoc_email",
"adHocEmail": "accountant@external.com",
"adHocName": "External Accountant",
"priority": 0
}
Databaseβ
Table: business_communication_recipient_rule
Key indexes:
idx_recipient_rule_businessβ business_ididx_recipient_rule_type_channelβ (communication_type, channel)idx_recipient_rule_activeβ is_active
Migration: 2025-10-26t00:00:00.000z-communication-recipient-targeting.mjs
Validationβ
Validation is split between two layers:
- DTO validators (class-validator decorators) β format validation (email, phone, UUID), mutually exclusive fields, required fields per targeting type
- Service layer β business rule validation (group exists and is active), contact normalization (email lowercase, phone E.164)
Design Decisionsβ
- Polymorphic targeting via nullable columns β One table with nullable
roleName,groupId,adHocEmail, etc. Only one is populated per rule. Simpler than separate tables per targeting type. - Validation split β DTO handles format; service handles business rules. Avoids duplicating validation.
- Repository owns
updatedAtβ The infrastructure layer setsupdatedAton all mutations, keeping timestamp management in one place. - Interface-only exports β Module exports the
"IRecipientRulesRepository"DI token, never the concrete class, preserving hexagonal boundaries.