Skip to main content

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 existence
  • CommunicationsModule (forward ref) β€” RecipientResolverService for preview

Domain Concepts​

Targeting Types​

Each rule uses exactly one targeting strategy:

TypeFieldDescription
roleroleNameTargets all business users with a specific role
groupgroupIdTargets all members of a recipient group
ad_hoc_emailadHocEmailTargets a specific email address
ad_hoc_phoneadHocPhoneTargets a specific phone number (E.164)
ad_hoc_whatsappadHocWhatsappTargets 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​

MethodPathDescription
POST/recipient-rulesCreate a new rule
GET/recipient-rules/business/:businessIdList all rules for a business
GET/recipient-rules/business/:businessId/previewPreview resolved recipients
GET/recipient-rules/:idGet a single rule
PATCH/recipient-rules/:idUpdate rule (priority, isActive)
DELETE/recipient-rules/:idHard delete a rule
POST/recipient-rules/:id/deactivateSoft 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 preview
  • channel (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_id
  • idx_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:

  1. DTO validators (class-validator decorators) β€” format validation (email, phone, UUID), mutually exclusive fields, required fields per targeting type
  2. Service layer β€” business rule validation (group exists and is active), contact normalization (email lowercase, phone E.164)

Design Decisions​

  1. 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.
  2. Validation split β€” DTO handles format; service handles business rules. Avoids duplicating validation.
  3. Repository owns updatedAt β€” The infrastructure layer sets updatedAt on all mutations, keeping timestamp management in one place.
  4. Interface-only exports β€” Module exports the "IRecipientRulesRepository" DI token, never the concrete class, preserving hexagonal boundaries.