Saltar al contenido principal

Communication Recipient Targeting System

Overview

This document describes the flexible recipient targeting system that allows businesses to control WHO receives each type of communication through any channel (email, SMS, WhatsApp).

Problem Statement

Previously, the system could only send communications to:

  • All business users (no filtering)
  • Specific individual recipients (one at a time)

Businesses need:

  1. ✅ Send to specific roles (e.g., all managers, all inventory managers)
  2. ✅ Create custom groups with mixed users, emails, and phone numbers
  3. ✅ Include ad-hoc recipients (external emails/phones not in the system)
  4. Combine all the above (roles + groups + ad-hoc for one communication type)

Architecture

Core Tables

1. communication_recipient_group

Custom groups businesses can create to organize recipients.

Example Groups:

  • "Finance Team" (3 finance managers + external accountant email)
  • "Purchasing Department" (inventory managers + 2 supplier contacts)
  • "Emergency Contacts" (owners + security service phone numbers)
CREATE TABLE communication_recipient_group (
id UUID PRIMARY KEY,
business_id UUID NOT NULL REFERENCES business(id),
name VARCHAR NOT NULL, -- "Finance Team"
description TEXT, -- Purpose/details
code VARCHAR, -- Optional system code
is_active BOOLEAN DEFAULT TRUE,
tags TEXT[], -- Organization/filtering
created_by UUID REFERENCES user(id),
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(business_id, name)
);

2. communication_group_member

Members of recipient groups (polymorphic: users OR emails OR phones).

Member Types:

  • business_user: Link to existing business user
  • email: External email address
  • phone: Phone number for SMS
  • whatsapp: WhatsApp number
CREATE TYPE recipient_member_type AS ENUM (
'business_user', 'email', 'phone', 'whatsapp'
);

CREATE TABLE communication_group_member (
id UUID PRIMARY KEY,
group_id UUID NOT NULL REFERENCES communication_recipient_group(id),
member_type recipient_member_type NOT NULL,

-- Polymorphic references (only ONE filled based on member_type)
business_user_id UUID REFERENCES business_user(id),
email_address VARCHAR,
phone_number VARCHAR,
display_name VARCHAR,

is_active BOOLEAN DEFAULT TRUE,
added_by UUID REFERENCES user(id),
added_at TIMESTAMPTZ DEFAULT NOW()
);

3. business_communication_recipient_rule

Links communication configs to recipient targeting rules.

Targeting Types:

  • role: All users with a specific role
  • group: All members of a custom group
  • ad_hoc_email: Single email address
  • ad_hoc_phone: Single phone number
  • ad_hoc_whatsapp: Single WhatsApp number
CREATE TYPE recipient_targeting_type AS ENUM (
'role', 'group', 'ad_hoc_email', 'ad_hoc_phone', 'ad_hoc_whatsapp'
);

CREATE TABLE business_communication_recipient_rule (
id UUID PRIMARY KEY,
business_id UUID NOT NULL,
communication_type communication_type NOT NULL,
channel communication_channel NOT NULL,

targeting_type recipient_targeting_type NOT NULL,

-- Polymorphic references (only ONE filled based on targeting_type)
role_name unique_role_names, -- For 'role' type
group_id UUID REFERENCES communication_recipient_group(id),
ad_hoc_email VARCHAR,
ad_hoc_phone VARCHAR,
ad_hoc_whatsapp VARCHAR,
ad_hoc_name VARCHAR,

priority INTEGER DEFAULT 0,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW()
);

4. communication_recipient_log

Audit trail of recipient selection (for compliance and debugging).

CREATE TABLE communication_recipient_log (
id UUID PRIMARY KEY,
communication_id UUID REFERENCES communication(id),

recipient_type VARCHAR NOT NULL,
recipient_id UUID,
recipient_contact VARCHAR NOT NULL,
recipient_name VARCHAR,

selection_method VARCHAR, -- 'role', 'group', 'ad_hoc'
selection_source_id UUID, -- Which rule selected them
selection_details JSON,

selected_at TIMESTAMPTZ DEFAULT NOW()
);

Data Flow

Example: Low Stock Alert

graph TB
A[Low Stock Alert Created] --> B[RecipientResolverService]
B --> C{Query Recipient Rules}
C --> D[Rule 1: role = 'inventory_manager']
C --> E[Rule 2: group = 'Purchasing Team']
C --> F[Rule 3: ad_hoc_email = 'supplier@vendor.com']

D --> G[Resolve: Get all inventory managers]
E --> H[Resolve: Get all group members]
F --> I[Resolve: Use email directly]

G --> J[Deduplicate Recipients]
H --> J
I --> J

J --> K[Send Communication to Each]
K --> L[Log Recipients Selected]

Usage Examples

Example 1: Low Stock Alerts

Requirement: Send low stock alerts to:

  • All inventory managers (role)
  • Purchasing team group (3 staff + 1 external supplier email)
  • Owner's personal phone (ad-hoc)

Configuration:

-- 1. Create Purchasing Team group
INSERT INTO communication_recipient_group (business_id, name, description)
VALUES ('business-123', 'Purchasing Team', 'Team responsible for inventory purchasing');

-- 2. Add members to group
INSERT INTO communication_group_member (group_id, member_type, business_user_id)
VALUES ('group-456', 'business_user', 'user-1'),
('group-456', 'business_user', 'user-2'),
('group-456', 'business_user', 'user-3');

INSERT INTO communication_group_member (group_id, member_type, email_address, display_name)
VALUES ('group-456', 'email', 'supplier@vendor.com', 'Main Supplier Contact');

-- 3. Create recipient rules for low_stock_alert
INSERT INTO business_communication_recipient_rule
(business_id, communication_type, channel, targeting_type, role_name, priority)
VALUES ('business-123', 'low_stock_alert', 'email', 'role', 'inventory_manager', 1);

INSERT INTO business_communication_recipient_rule
(business_id, communication_type, channel, targeting_type, group_id, priority)
VALUES ('business-123', 'low_stock_alert', 'email', 'group', 'group-456', 2);

INSERT INTO business_communication_recipient_rule
(business_id, communication_type, channel, targeting_type, ad_hoc_phone, ad_hoc_name, priority)
VALUES ('business-123', 'low_stock_alert', 'sms', 'ad_hoc_phone', '+1234567890', 'Owner Mobile', 3);

Result:

  • Email sent to all inventory managers
  • Email sent to 3 purchasing staff + supplier
  • SMS sent to owner's phone

Example 2: Daily Sales Reports

Requirement: Send daily reports to:

  • Finance group (accountant + 2 managers)
  • Owner
  • External accountant email
-- 1. Create Finance group
INSERT INTO communication_recipient_group (business_id, name)
VALUES ('business-123', 'Finance Group');

-- 2. Add members
INSERT INTO communication_group_member (group_id, member_type, business_user_id)
VALUES ('finance-group', 'business_user', 'accountant-user'),
('finance-group', 'business_user', 'manager-1'),
('finance-group', 'business_user', 'manager-2');

-- 3. Create rules
INSERT INTO business_communication_recipient_rule
(business_id, communication_type, channel, targeting_type, group_id)
VALUES ('business-123', 'daily_report', 'email', 'group', 'finance-group');

INSERT INTO business_communication_recipient_rule
(business_id, communication_type, channel, targeting_type, role_name)
VALUES ('business-123', 'daily_report', 'email', 'role', 'owner');

INSERT INTO business_communication_recipient_rule
(business_id, communication_type, channel, targeting_type, ad_hoc_email, ad_hoc_name)
VALUES ('business-123', 'daily_report', 'email', 'ad_hoc_email', 'cpa@accounting-firm.com', 'External CPA');

Example 3: Emergency Alerts (Multi-Channel)

Requirement: Send emergency alerts via BOTH email AND SMS to:

  • All owners
  • All admins
  • Security service phone
  • Manager group
-- Email channel rules
INSERT INTO business_communication_recipient_rule
(business_id, communication_type, channel, targeting_type, role_name)
VALUES ('business-123', 'emergency_alert', 'email', 'role', 'owner'),
('business-123', 'emergency_alert', 'email', 'role', 'admin');

-- SMS channel rules
INSERT INTO business_communication_recipient_rule
(business_id, communication_type, channel, targeting_type, role_name)
VALUES ('business-123', 'emergency_alert', 'sms', 'role', 'owner'),
('business-123', 'emergency_alert', 'sms', 'role', 'admin');

INSERT INTO business_communication_recipient_rule
(business_id, communication_type, channel, targeting_type, ad_hoc_phone, ad_hoc_name)
VALUES ('business-123', 'emergency_alert', 'sms', 'ad_hoc_phone', '+1-555-SECURITY', 'Security Service');

Implementation Guide

Step 1: Create RecipientResolverService

@Injectable()
export class RecipientResolverService {
constructor(
private recipientRuleRepository: RecipientRuleRepository,
private recipientGroupRepository: RecipientGroupRepository,
private businessUsersService: BusinessUsersService,
) {}

/**
* Resolve all recipients for a communication
*/
async resolveRecipients(
businessId: string,
communicationType: CommunicationType,
channel: CommunicationChannel,
additionalFilters?: RecipientFilters,
): Promise<ResolvedRecipient[]> {
// 1. Get all active recipient rules for this business + type + channel
const rules = await this.recipientRuleRepository.findActiveRules({
businessId,
communicationType,
channel,
});

// 2. Resolve each rule to actual recipients
const recipientSets = await Promise.all(
rules.map((rule) => this.resolveRule(rule, additionalFilters)),
);

// 3. Flatten and deduplicate
const allRecipients = recipientSets.flat();
const deduplicated = this.deduplicateRecipients(allRecipients);

// 4. Apply any additional filters (e.g., location-specific)
return this.applyFilters(deduplicated, additionalFilters);
}

/**
* Resolve a single rule to recipients
*/
private async resolveRule(
rule: RecipientRule,
filters?: RecipientFilters,
): Promise<ResolvedRecipient[]> {
switch (rule.targetingType) {
case 'role':
return this.resolveRoleRecipients(rule.roleName, rule.businessId);

case 'group':
return this.resolveGroupRecipients(rule.groupId);

case 'ad_hoc_email':
return [{
type: 'email',
contact: rule.adHocEmail,
name: rule.adHocName,
source: { ruleId: rule.id, method: 'ad_hoc' },
}];

case 'ad_hoc_phone':
return [{
type: 'phone',
contact: rule.adHocPhone,
name: rule.adHocName,
source: { ruleId: rule.id, method: 'ad_hoc' },
}];

case 'ad_hoc_whatsapp':
return [{
type: 'whatsapp',
contact: rule.adHocWhatsapp,
name: rule.adHocName,
source: { ruleId: rule.id, method: 'ad_hoc' },
}];
}
}

/**
* Resolve role to all users with that role
*/
private async resolveRoleRecipients(
roleName: string,
businessId: string,
): Promise<ResolvedRecipient[]> {
const users = await this.businessUsersService.findByRole(businessId, roleName);

return users.map((user) => ({
type: 'business_user',
userId: user.userId,
contact: user.email, // or phone based on channel
name: user.fullName,
source: { method: 'role', roleName },
}));
}

/**
* Resolve group to all members
*/
private async resolveGroupRecipients(
groupId: string,
): Promise<ResolvedRecipient[]> {
const members = await this.recipientGroupRepository.findGroupMembers(groupId);

return members.map((member) => {
if (member.memberType === 'business_user') {
return {
type: 'business_user',
userId: member.businessUserId,
contact: member.user.email, // Join with user table
name: member.user.fullName,
source: { method: 'group', groupId },
};
} else {
return {
type: member.memberType,
contact: member.emailAddress || member.phoneNumber,
name: member.displayName,
source: { method: 'group', groupId },
};
}
});
}

/**
* Deduplicate recipients (same person via multiple rules)
*/
private deduplicateRecipients(
recipients: ResolvedRecipient[],
): ResolvedRecipient[] {
const seen = new Map<string, ResolvedRecipient>();

for (const recipient of recipients) {
const key = `${recipient.type}:${recipient.contact}`;
if (!seen.has(key)) {
seen.set(key, recipient);
}
}

return Array.from(seen.values());
}
}

Step 2: Update Event Handlers

@Injectable()
export class OnCreateLowStockAlertHandler {
constructor(
private readonly communicationsService: CommunicationsService,
private readonly recipientResolverService: RecipientResolverService,
) {}

async handleOnCreateLowStockAlert(event: OnCreateLowStockAlertEvent) {
const { createdLowStockAlertRecord } = event;

// Resolve recipients based on rules
const recipients = await this.recipientResolverService.resolveRecipients(
createdLowStockAlertRecord.businessId,
'low_stock_alert',
'email', // or iterate through all enabled channels
);

// Send to each recipient
for (const recipient of recipients) {
await this.communicationsService.send({
businessId: createdLowStockAlertRecord.businessId,
channel: 'email',
type: 'low_stock_alert',
recipientType: recipient.type,
recipientId: recipient.userId,
recipientContact: recipient.contact,
// ... other fields
});
}
}
}

Step 3: Create Management APIs

Group Management

  • POST /api/recipient-groups - Create group
  • GET /api/recipient-groups?businessId=xxx - List groups
  • PUT /api/recipient-groups/:id - Update group
  • DELETE /api/recipient-groups/:id - Delete group
  • POST /api/recipient-groups/:id/members - Add member
  • DELETE /api/recipient-groups/:id/members/:memberId - Remove member

Recipient Rule Management

  • POST /api/communication-recipient-rules - Create rule
  • GET /api/communication-recipient-rules?businessId=xxx&type=low_stock_alert - List rules
  • PUT /api/communication-recipient-rules/:id - Update rule
  • DELETE /api/communication-recipient-rules/:id - Delete rule
  • POST /api/communication-recipient-rules/preview - Preview recipients for a rule

Benefits

1. Flexibility

  • Mix roles, groups, and ad-hoc recipients
  • Different targeting per communication type
  • Different targeting per channel

2. Scalability

  • Add new communication types without code changes
  • Businesses configure their own rules
  • Easy to extend with new targeting types

3. Maintainability

  • Clear separation of concerns
  • Audit trail of recipient selection
  • Easy to debug why someone received/didn't receive a message

4. Business Control

  • Business admins manage their own groups
  • No developer involvement for new recipients
  • Self-service configuration

Migration Path

For Existing Systems

  1. Create tables (run migration)

  2. Create default rules for existing communications:

    -- Convert "send to all" to "send to admin + owner roles"
    INSERT INTO business_communication_recipient_rule
    (business_id, communication_type, channel, targeting_type, role_name)
    SELECT DISTINCT business_id, 'low_stock_alert', 'email', 'role', 'admin'
    FROM business;
  3. Deploy RecipientResolverService

  4. Update event handlers one at a time

  5. Build admin UI for self-service management

Future Enhancements

  1. Location-based filtering - Send only to users at specific locations
  2. Time-based rules - Send to different people at different times
  3. Escalation chains - If no response, escalate to next group
  4. Recipient preferences - Let users opt-out of specific communication types
  5. Smart groups - Dynamic groups based on criteria (e.g., "all managers at locations with low stock")
  6. Templates per group - Different message templates for different recipient groups

Database Diagram

┌─────────────────────────────────┐
│ business_communication_config │
│ (type + channel settings) │
└────────────┬────────────────────┘

│ 1:N

┌─────────────────────────────────┐
│ business_communication_ │◄─────┐
│ recipient_rule │ │
│ (WHO receives this type) │ │
└─────────┬───────────┬───────────┘ │
│ │ │
role │ │ group │ ad_hoc
│ │ │
▼ ▼ │
┌──────────┐ ┌────────────────┐ │
│ Roles │ │ recipient_ │ │
│ (enum) │ │ group │ │
└──────────┘ └────┬───────────┘ │
│ 1:N │
▼ │
┌─────────────────┐ │
│ group_member │────┘
│ (users/emails/ │
│ phones) │
└─────────────────┘

Conclusion

This recipient targeting system provides a flexible, scalable solution for controlling who receives communications in your business. It supports role-based, group-based, and ad-hoc targeting with full auditability and easy maintenance.

The polymorphic design allows for future extensions without schema changes, and the service-based resolution makes it easy to integrate with any communication event handler.