Recipient Targeting System - Implementation Checklist
Overviewβ
This checklist guides you through implementing the flexible recipient targeting system for the communication module.
Phase 1: Database Setup β β
1.1 Run Migrationβ
- Review migration file:
2025-10-26t00:00:00.000z-communication-recipient-targeting.mjs - Backup production database
- Run migration in development
- Test rollback (down migration)
- Run migration in staging
- Run migration in production
1.2 Verify Tables Createdβ
-- Verify all 4 tables exist
SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name IN (
'communication_recipient_group',
'communication_group_member',
'business_communication_recipient_rule',
'communication_recipient_log'
);
-- Verify enums created
SELECT typname FROM pg_type
WHERE typname IN ('recipient_member_type', 'recipient_targeting_type');
1.3 Test Constraintsβ
- Test unique constraint on group names per business
- Test foreign key cascades (delete business β deletes rules)
- Test foreign key cascades (delete group β deletes members)
Phase 2: Backend - Domain & Repository Layer π¦β
2.1 Create Domain Typesβ
File: packages/backend/database/src/types/recipient-targeting.types.ts
export type RecipientMemberType = 'business_user' | 'email' | 'phone' | 'whatsapp';
export type RecipientTargetingType = 'role' | 'group' | 'ad_hoc_email' | 'ad_hoc_phone' | 'ad_hoc_whatsapp';
export interface RecipientGroup {
id: string;
businessId: string;
name: string;
description?: string;
isActive: boolean;
createdAt: Date;
}
export interface GroupMember {
id: string;
groupId: string;
memberType: RecipientMemberType;
businessUserId?: string;
emailAddress?: string;
phoneNumber?: string;
displayName?: string;
isActive: boolean;
}
export interface RecipientRule {
id: string;
businessId: string;
communicationType: string;
channel: string;
targetingType: RecipientTargetingType;
roleName?: string;
groupId?: string;
adHocEmail?: string;
adHocPhone?: string;
adHocWhatsapp?: string;
adHocName?: string;
priority: number;
isActive: boolean;
}
export interface ResolvedRecipient {
type: 'business_user' | 'email' | 'phone' | 'whatsapp';
userId?: string;
contact: string;
name?: string;
source: {
method: 'role' | 'group' | 'ad_hoc' | 'manual';
ruleId?: string;
groupId?: string;
roleName?: string;
};
}
Checklist:
- Create type definitions
- Export from index
- Update database.types.ts with new tables
- Regenerate database types if using kysely-codegen
2.2 Create Repositoriesβ
RecipientGroupRepositoryβ
File: apps/backend/src/recipient-groups/infrastructure/recipient-groups.repository.ts
@Injectable()
export class RecipientGroupsRepository {
constructor(@Inject(DATABASE) private readonly db: KyselyDatabase) {}
async create(data: InsertableRecipientGroup): Promise<SelectableRecipientGroup> { }
async findById(id: string): Promise<SelectableRecipientGroup | null> { }
async findByBusinessId(businessId: string): Promise<SelectableRecipientGroup[]> { }
async update(id: string, data: UpdateableRecipientGroup): Promise<void> { }
async delete(id: string): Promise<void> { }
// Member management
async addMember(groupId: string, member: InsertableGroupMember): Promise<SelectableGroupMember> { }
async findGroupMembers(groupId: string): Promise<SelectableGroupMember[]> { }
async removeMember(memberId: string): Promise<void> { }
}
Checklist:
- Create repository file
- Implement CRUD methods
- Implement member management
- Add proper error handling
- Add logging
- Write unit tests
RecipientRuleRepositoryβ
File: apps/backend/src/recipient-rules/infrastructure/recipient-rules.repository.ts
@Injectable()
export class RecipientRulesRepository {
constructor(@Inject(DATABASE) private readonly db: KyselyDatabase) {}
async create(data: InsertableRecipientRule): Promise<SelectableRecipientRule> { }
async findById(id: string): Promise<SelectableRecipientRule | null> { }
// Key method: Find all rules for a business + type + channel
async findActiveRules(filters: {
businessId: string;
communicationType: string;
channel: string;
}): Promise<SelectableRecipientRule[]> { }
async update(id: string, data: UpdateableRecipientRule): Promise<void> { }
async delete(id: string): Promise<void> { }
}
Checklist:
- Create repository file
- Implement CRUD methods
- Implement findActiveRules with proper filtering
- Add proper error handling
- Add logging
- Write unit tests
RecipientLogRepositoryβ
File: apps/backend/src/recipient-log/infrastructure/recipient-log.repository.ts
@Injectable()
export class RecipientLogRepository {
constructor(@Inject(DATABASE) private readonly db: KyselyDatabase) {}
async logRecipient(data: {
communicationId: string;
recipientType: string;
recipientId?: string;
recipientContact: string;
recipientName?: string;
selectionMethod: string;
selectionSourceId?: string;
selectionDetails?: any;
}): Promise<void> { }
async findByCommunication(communicationId: string): Promise<SelectableRecipientLog[]> { }
}
Checklist:
- Create repository file
- Implement logging methods
- Implement query methods
- Write unit tests
Phase 3: Backend - Service Layer π§β
3.1 Create RecipientResolverServiceβ
File: apps/backend/src/communications/application/services/recipient-resolver.service.ts
Key Methods:
@Injectable()
export class RecipientResolverService {
async resolveRecipients(
businessId: string,
communicationType: string,
channel: string,
): Promise<ResolvedRecipient[]> { }
private async resolveRule(rule: RecipientRule): Promise<ResolvedRecipient[]> { }
private async resolveRoleRecipients(roleName: string, businessId: string): Promise<ResolvedRecipient[]> { }
private async resolveGroupRecipients(groupId: string): Promise<ResolvedRecipient[]> { }
private deduplicateRecipients(recipients: ResolvedRecipient[]): ResolvedRecipient[] { }
}
Implementation Steps:
- Create service file
- Implement resolveRecipients (main method)
- Implement resolveRule (switch on targeting type)
- Implement resolveRoleRecipients (query business users by role)
- Implement resolveGroupRecipients (query group members + join users)
- Implement deduplication logic (same contact)
- Add logging at each step
- Write comprehensive unit tests
- Write integration tests
Unit Tests:
- Test role resolution with mock data
- Test group resolution with mixed member types
- Test ad-hoc resolution
- Test deduplication (same email via 2 rules)
- Test empty rules (no recipients)
- Test inactive rules (should be skipped)
3.2 Create RecipientGroupsServiceβ
File: apps/backend/src/recipient-groups/application/recipient-groups.service.ts
@Injectable()
export class RecipientGroupsService {
async createGroup(data: CreateGroupDto): Promise<RecipientGroup> { }
async getGroupsByBusiness(businessId: string): Promise<RecipientGroup[]> { }
async updateGroup(id: string, data: UpdateGroupDto): Promise<void> { }
async deleteGroup(id: string): Promise<void> { }
async addMember(groupId: string, member: AddMemberDto): Promise<GroupMember> { }
async getGroupMembers(groupId: string): Promise<GroupMember[]> { }
async removeMember(memberId: string): Promise<void> { }
}
Checklist:
- Create service file
- Implement all methods
- Add validation (unique names, valid emails/phones)
- Add authorization checks (user belongs to business)
- Write unit tests
3.3 Create RecipientRulesServiceβ
File: apps/backend/src/recipient-rules/application/recipient-rules.service.ts
@Injectable()
export class RecipientRulesService {
async createRule(data: CreateRuleDto): Promise<RecipientRule> { }
async getRules(filters: GetRulesDto): Promise<RecipientRule[]> { }
async updateRule(id: string, data: UpdateRuleDto): Promise<void> { }
async deleteRule(id: string): Promise<void> { }
// Preview feature: See who will receive without saving
async previewRecipients(data: PreviewRecipientsDto): Promise<ResolvedRecipient[]> { }
}
Checklist:
- Create service file
- Implement all methods
- Implement preview feature using RecipientResolverService
- Add validation
- Add authorization checks
- Write unit tests
Phase 4: Backend - API Layer πβ
4.1 Create RecipientGroupsControllerβ
File: apps/backend/src/recipient-groups/interfaces/recipient-groups.controller.ts
Endpoints:
@Controller('recipient-groups')
export class RecipientGroupsController {
@Post()
async createGroup(@Body() dto: CreateGroupDto) { }
@Get()
async getGroups(@Query('businessId') businessId: string) { }
@Get(':id')
async getGroupById(@Param('id') id: string) { }
@Put(':id')
async updateGroup(@Param('id') id: string, @Body() dto: UpdateGroupDto) { }
@Delete(':id')
async deleteGroup(@Param('id') id: string) { }
@Post(':id/members')
async addMember(@Param('id') groupId: string, @Body() dto: AddMemberDto) { }
@Get(':id/members')
async getMembers(@Param('id') groupId: string) { }
@Delete(':id/members/:memberId')
async removeMember(@Param('memberId') memberId: string) { }
}
Checklist:
- Create controller file
- Create DTOs with validation
- Implement all endpoints
- Add authentication guards
- Add authorization checks (business ownership)
- Add API documentation (Swagger)
- Write e2e tests
4.2 Create RecipientRulesControllerβ
File: apps/backend/src/recipient-rules/interfaces/recipient-rules.controller.ts
Endpoints:
@Controller('communication-recipient-rules')
export class RecipientRulesController {
@Post()
async createRule(@Body() dto: CreateRuleDto) { }
@Get()
async getRules(@Query() query: GetRulesQuery) { }
@Get(':id')
async getRuleById(@Param('id') id: string) { }
@Put(':id')
async updateRule(@Param('id') id: string, @Body() dto: UpdateRuleDto) { }
@Delete(':id')
async deleteRule(@Param('id') id: string) { }
@Post('preview')
async previewRecipients(@Body() dto: PreviewRecipientsDto) { }
}
Checklist:
- Create controller file
- Create DTOs with validation
- Implement all endpoints
- Add authentication guards
- Add authorization checks
- Add API documentation
- Write e2e tests
4.3 Create DTOsβ
CreateGroupDto:
export class CreateGroupDto {
@IsUUID()
businessId: string;
@IsString()
@MinLength(3)
name: string;
@IsOptional()
@IsString()
description?: string;
}
AddMemberDto:
export class AddMemberDto {
@IsEnum(['business_user', 'email', 'phone', 'whatsapp'])
memberType: RecipientMemberType;
@ValidateIf(o => o.memberType === 'business_user')
@IsUUID()
businessUserId?: string;
@ValidateIf(o => o.memberType === 'email')
@IsEmail()
emailAddress?: string;
@ValidateIf(o => o.memberType === 'phone' || o.memberType === 'whatsapp')
@IsPhoneNumber()
phoneNumber?: string;
@IsOptional()
@IsString()
displayName?: string;
}
CreateRuleDto:
export class CreateRuleDto {
@IsUUID()
businessId: string;
@IsEnum(CommunicationType)
communicationType: string;
@IsEnum(['email', 'sms', 'whatsapp'])
channel: string;
@IsEnum(['role', 'group', 'ad_hoc_email', 'ad_hoc_phone', 'ad_hoc_whatsapp'])
targetingType: RecipientTargetingType;
@ValidateIf(o => o.targetingType === 'role')
@IsString()
roleName?: string;
@ValidateIf(o => o.targetingType === 'group')
@IsUUID()
groupId?: string;
// ... other fields
}
Checklist:
- Create all DTOs
- Add validation decorators
- Add conditional validation (ValidateIf)
- Add API documentation decorators
- Write validation tests
Phase 5: Update Event Handlers πβ
5.1 Update OnCreateLowStockAlertHandlerβ
File: apps/backend/src/communications/application/events/on-create-low-stock-alert.handler.ts
Changes:
// OLD:
const businessUsers = await this.businessUsersService.findByBusinessId(businessId);
for (const user of businessUsers) { /* send */ }
// NEW:
const recipients = await this.recipientResolverService.resolveRecipients(
businessId,
'low_stock_alert',
'email'
);
for (const recipient of recipients) {
await this.communicationsService.send({
recipientType: recipient.type,
recipientId: recipient.userId,
recipientContact: recipient.contact,
recipientName: recipient.name,
// ... other fields
});
// Log recipient selection
await this.recipientLogRepository.logRecipient({
communicationId: communication.id,
recipientContact: recipient.contact,
selectionMethod: recipient.source.method,
selectionSourceId: recipient.source.ruleId,
});
}
Checklist:
- Inject RecipientResolverService
- Inject RecipientLogRepository
- Replace hardcoded recipient logic
- Add recipient logging
- Test with different rule configurations
- Test with no rules (fallback behavior?)
- Update handler documentation
5.2 Update Other Handlersβ
Repeat for each communication type:
- Invoice handler
- Collection notice handler
- Daily report handler
- Emergency alert handler
- Any other communication handlers
Phase 6: Admin UI - Frontend π¨β
6.1 Create Group Management Pagesβ
Pages:
-
/settings/recipient-groups- List all groups -
/settings/recipient-groups/new- Create new group -
/settings/recipient-groups/:id- View/edit group details -
/settings/recipient-groups/:id/members- Manage members
Components:
- GroupListTable
- GroupForm
- MemberListTable
- AddMemberModal
- MemberTypeSelector
Features:
- List groups with member counts
- Create/edit/delete groups
- Add/remove members
- Search/filter groups
- Validate emails/phone numbers
- Show member preview (users vs external contacts)
6.2 Create Recipient Rule Configuration Pagesβ
Pages:
-
/settings/communication-rules- Overview of all rules -
/settings/communication-rules/low-stock-alert- Rules for specific type -
/settings/communication-rules/configure- Configure rules wizard
Components:
- RuleConfigurationMatrix (grid: types Γ channels)
- RulesList
- RuleForm
- RecipientPreview (show who will receive)
- TargetingTypeSelector (role/group/ad-hoc)
Features:
- Visual matrix of communication types Γ channels
- Configure rules per type/channel
- Add multiple rules (role + group + ad-hoc)
- Preview recipients before saving
- Drag to reorder rules (priority)
- Enable/disable rules
6.3 Integrate with Existing Settingsβ
Add new sections to business settings:
- Add "Recipient Groups" menu item
- Add "Communication Rules" menu item
- Link from communication config page
- Show recipient count in communication overview
Phase 7: Testing π§ͺβ
7.1 Unit Testsβ
- Repository layer tests (all methods)
- Service layer tests (all methods)
- RecipientResolverService tests (critical!)
- Controller tests (all endpoints)
- DTO validation tests
7.2 Integration Testsβ
- Full recipient resolution flow
- Role resolution with real database
- Group resolution with mixed members
- Deduplication logic
- Logging recipient selection
7.3 E2E Testsβ
- Create group via API
- Add members to group
- Create recipient rules
- Preview recipients
- Trigger communication (low stock alert)
- Verify correct recipients received messages
- Verify recipient logs created
7.4 Manual Testing Scenariosβ
- Business A: Send low stock to inventory_manager role only
- Business B: Send low stock to custom "Purchasing Team" group
- Business C: Send low stock to role + group + ad-hoc phone
- Empty rules: What happens? (should have fallback)
- Inactive rules: Should be skipped
- Deleted group: Rules should cascade delete
- Duplicate recipients: Should be deduplicated
Phase 8: Documentation πβ
8.1 Technical Documentationβ
- Architecture design document
- Database schema documentation
- API documentation (Swagger)
- Service documentation (JSDoc)
- Integration guide for new handlers
8.2 User Documentationβ
- Admin guide: How to create groups
- Admin guide: How to configure recipient rules
- Admin guide: Best practices
- Video tutorial (optional)
8.3 Developer Documentationβ
- Migration guide
- Code examples
- Troubleshooting guide
Phase 9: Deployment πβ
9.1 Pre-Deploymentβ
- Review all code changes
- Run full test suite
- Code review completed
- Database migration tested in staging
- Backup plan documented
9.2 Deployment Stepsβ
- Deploy database migration (off-peak hours)
- Deploy backend changes
- Deploy frontend changes
- Verify migration success
- Test critical paths
9.3 Post-Deploymentβ
- Monitor error logs
- Monitor recipient resolution performance
- Monitor database query performance
- Verify communications still working
- User acceptance testing
9.4 Rollback Plan (If Needed)β
- Database rollback script ready
- Code rollback plan
- Communication plan for users
Phase 10: Training & Rollout π₯β
10.1 Internal Trainingβ
- Train support team on new features
- Train sales team for demos
- Create internal FAQ
10.2 Customer Rolloutβ
- Announce new feature (release notes)
- Create default rules for existing businesses
- Offer migration assistance
- Collect feedback
Success Criteria β¨β
Functional Requirementsβ
- β Businesses can create custom recipient groups
- β Groups support mixed member types (users + emails + phones)
- β Businesses can configure rules per communication type/channel
- β Rules support role + group + ad-hoc targeting
- β Multiple rules can be combined
- β Recipients are deduplicated
- β Recipient selection is logged for audit
Non-Functional Requirementsβ
- β Recipient resolution takes < 500ms
- β No breaking changes to existing communications
- β Admin UI is intuitive (< 5 min to configure)
- β System is scalable (handles 100+ groups per business)
- β Full audit trail for compliance
Timeline Estimateβ
| Phase | Estimated Time |
|---|---|
| Phase 1: Database | 1 day |
| Phase 2: Repositories | 2-3 days |
| Phase 3: Services | 3-4 days |
| Phase 4: APIs | 2-3 days |
| Phase 5: Update Handlers | 2 days |
| Phase 6: Frontend UI | 5-7 days |
| Phase 7: Testing | 3-4 days |
| Phase 8: Documentation | 2 days |
| Phase 9: Deployment | 1 day |
| Phase 10: Rollout | 1-2 days |
| Total | 22-29 days (1-1.5 sprints) |
Notes & Considerationsβ
Performance Optimizationβ
- Consider caching recipient resolution results (TTL: 5 min)
- Add database indexes on frequently queried columns
- Optimize query for resolving group members (reduce N+1)
Edge Casesβ
- What if no rules configured? β Fallback to existing behavior (all business users)
- What if all rules return no recipients? β Log warning, skip sending
- What if ad-hoc email is invalid? β Validate before saving, skip invalid ones
- What if user is in multiple groups? β Deduplicate in RecipientResolverService
Future Enhancementsβ
- Location-based filtering (send only to specific locations)
- Time-based rules (different recipients at different times)
- Escalation chains (if no response, send to next group)
- Smart groups (dynamic based on criteria)
- Recipient preferences (opt-out per type)