Saltar al contenido principal

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.