CRM Service Event Catalog
Version: 1.0 Date: December 2025
Overview
This document catalogs all events consumed and produced by the CRM service. Events are the primary mechanism for keeping the CRM in sync with source services (SCL, TeeTime, Messaging, Feedback).
Event Architecture
┌─────────────────────────────────────────────────────────────────────────────┐
│ EVENT FLOW DIAGRAM │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ SOURCE SERVICES │ │
│ ├─────────────┬─────────────┬─────────────────┬─────────────────────┤ │
│ │ │ │ │ │ │
│ │ SCL Service │ TeeTime Svc │ Messaging Svc │ Feedback Service │ │
│ │ │ │ │ │ │
│ │ DomainOutbox│ DomainOutbox│ EventEmitter │ EventEmitter │ │
│ │ │ │ │ │ │
│ └──────┬──────┴──────┬──────┴────────┬────────┴──────────┬──────────┘ │
│ │ │ │ │ │
│ │ member.* │ player.* │ contact.* │ feedback.* │
│ │ tier.* │ teetime.* │ message.* │ voted/unvoted │
│ │ payment.* │ handicap.* │ consent.* │ shipped │
│ │ │ competition.* │ campaign.* │ │
│ │ │ │ │ │
│ └─────────────┼───────────────┼───────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ REDIS / BULLMQ │ │
│ │ Queue: crm-events │ │
│ └─────────────────┬───────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ CRM SERVICE │ │
│ ├─────────────────────────────────────┤ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ Event Processor │ │ │
│ │ │ ───────────────────── │ │ │
│ │ │ 1. Parse event │ │ │
│ │ │ 2. Resolve identity │ │ │
│ │ │ 3. Update profile │ │ │
│ │ │ 4. Create activity │ │ │
│ │ │ 5. Trigger scoring │ │ │
│ │ │ 6. Update segments │ │ │
│ │ └─────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ CRM Database │ │ │
│ │ └─────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ Outbound Events │ │ │
│ │ │ (crm.profile.*) │ │ │
│ │ └─────────────────────────────┘ │ │
│ └─────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Event Envelope
All events follow a standard envelope format:
interface CrmEvent {
// Envelope metadata
id: string; // Unique event ID (UUID)
type: string; // Event type (e.g., "scl.member.created")
version: string; // Schema version (e.g., "1.0")
timestamp: string; // ISO 8601 timestamp
correlationId?: string; // For tracing related events
// Routing
tenantId: string; // Tenant identifier
source: string; // Source service name
// Identity hints for resolution
identity: {
authUserId?: string; // IDP user ID
email?: string; // Email address
phoneNumber?: string; // E.164 phone
sclMemberId?: number; // SCL Member ID
teetimePlayerId?: string; // TeeTime Player ID
messagingContactId?: string; // Messaging Contact ID
externalIds?: Record<string, string>;
};
// Event-specific payload
payload: Record<string, unknown>;
// Optional metadata
metadata?: {
userId?: string; // User who triggered the event
ipAddress?: string;
userAgent?: string;
};
}
Events Consumed
SCL Service Events
scl.member.created
Fired when a new member is created in SCL.
{
id: "evt_123",
type: "scl.member.created",
version: "1.0",
timestamp: "2025-12-14T10:00:00Z",
tenantId: "tenant_123",
source: "scl",
identity: {
sclMemberId: 5678,
email: "john@example.com",
authUserId: "auth0|123"
},
payload: {
id: 5678,
firstName: "John",
lastName: "Smith",
email: "john@example.com",
status: "ACTIVE",
membershipTierId: 1,
membershipTierName: "Gold",
dateOfBirth: "1985-03-15",
createdAt: "2025-12-14T10:00:00Z"
}
}
CRM Actions:
- Resolve identity → find or create CustomerProfile
- Link
sclMemberIdto profile - Sync member data (firstName, lastName, etc.)
- Create
MEMBER_CREATEDactivity - Set
lifecycleStagetoCUSTOMER
scl.member.updated
Fired when member details are updated.
{
type: "scl.member.updated",
identity: { sclMemberId: 5678 },
payload: {
id: 5678,
changes: ["email", "phoneNumber"],
email: "john.new@example.com",
phoneNumber: "+27829999999",
updatedAt: "2025-12-14T11:00:00Z"
}
}
CRM Actions:
- Find profile by
sclMemberId - Update changed fields
- Create
MEMBER_UPDATEDactivity - If email/phone changed, update identity aliases
scl.member.tier.changed
Fired when member's tier changes.
{
type: "scl.member.tier.changed",
identity: { sclMemberId: 5678 },
payload: {
id: 5678,
previousTierId: 2,
previousTierName: "Silver",
newTierId: 1,
newTierName: "Gold",
reason: "upgrade",
effectiveDate: "2025-12-14T00:00:00Z"
}
}
CRM Actions:
- Update
membershipTieron profile - Create
TIER_UPGRADEDorTIER_DOWNGRADEDactivity - Trigger engagement score refresh
- Check segment membership changes
scl.member.status.changed
Fired when member status changes (active/inactive/suspended).
{
type: "scl.member.status.changed",
identity: { sclMemberId: 5678 },
payload: {
id: 5678,
previousStatus: "ACTIVE",
newStatus: "INACTIVE",
reason: "non_payment",
effectiveDate: "2025-12-14T00:00:00Z"
}
}
CRM Actions:
- Update
membershipStatuson profile - If INACTIVE → update
lifecycleStagetoLAPSED - Create
MEMBERSHIP_SUSPENDEDorMEMBERSHIP_CANCELLEDactivity - Trigger churn risk recalculation
scl.payment.received
Fired when a payment is processed.
{
type: "scl.payment.received",
identity: { sclMemberId: 5678 },
payload: {
id: "payment_123",
memberId: 5678,
amount: 150000, // cents
currency: "ZAR",
type: "membership_renewal",
invoiceId: "inv_456",
paidAt: "2025-12-14T10:00:00Z"
}
}
CRM Actions:
- Increment
lifetimeValueon profile - Update
lastPaymentAt - Create
PAYMENT_RECEIVEDactivity - Trigger engagement score refresh
scl.payment.failed
Fired when a payment fails.
{
type: "scl.payment.failed",
identity: { sclMemberId: 5678 },
payload: {
id: "payment_123",
memberId: 5678,
amount: 150000,
reason: "insufficient_funds",
attemptedAt: "2025-12-14T10:00:00Z"
}
}
CRM Actions:
- Create
PAYMENT_FAILEDactivity - Increase
churnRiskScore
TeeTime Service Events
teetime.player.created
Fired when a new player is created.
{
type: "teetime.player.created",
identity: {
teetimePlayerId: "player_uuid_123",
email: "john@example.com",
phoneNumber: "+27821234567"
},
payload: {
id: "player_uuid_123",
name: "John",
surname: "Smith",
email: "john@example.com",
phone: "+27821234567",
handicap: 12.5,
homeClubId: "club_123",
homeClubName: "Randpark Golf Club",
createdAt: "2025-12-14T10:00:00Z"
}
}
CRM Actions:
- Resolve identity → find or create CustomerProfile
- Link
teetimePlayerIdto profile - Sync player data (handicap, homeClub)
- Create
PROFILE_CREATEDactivity if new
teetime.player.updated
Fired when player details are updated.
{
type: "teetime.player.updated",
identity: { teetimePlayerId: "player_uuid_123" },
payload: {
id: "player_uuid_123",
changes: ["handicap", "homeClubId"],
handicap: 11.8,
homeClubId: "club_456",
homeClubName: "Royal Johannesburg"
}
}
teetime.handicap.updated
Fired when handicap is updated (from association sync).
{
type: "teetime.handicap.updated",
identity: {
teetimePlayerId: "player_uuid_123",
externalIds: { golfRsaId: "RSA-2024-12345" }
},
payload: {
playerId: "player_uuid_123",
previousHandicap: 12.5,
newHandicap: 11.8,
source: "GOLF_RSA",
effectiveDate: "2025-12-14T00:00:00Z"
}
}
CRM Actions:
- Update
handicapandhandicapUpdatedAt - Create
HANDICAP_UPDATEDactivity
teetime.booking.created
Fired when a tee time is booked.
{
type: "teetime.booking.created",
identity: { teetimePlayerId: "player_uuid_123" },
payload: {
id: "booking_789",
playerId: "player_uuid_123",
clubId: "club_123",
clubName: "Randpark Golf Club",
courseId: "course_456",
courseName: "Bushwillow Course",
startTime: "2025-12-20T06:30:00Z",
players: 4,
status: "CONFIRMED",
bookedAt: "2025-12-14T10:00:00Z"
}
}
CRM Actions:
- Create
TEETIME_BOOKEDactivity - Increment
bookingCount30d,bookingCountTotal - Update
lastBookingAt,lastActivityAt
teetime.booking.completed
Fired when a tee time is completed (checked in/out).
{
type: "teetime.booking.completed",
identity: { teetimePlayerId: "player_uuid_123" },
payload: {
id: "booking_789",
playerId: "player_uuid_123",
clubId: "club_123",
clubName: "Randpark Golf Club",
startTime: "2025-12-20T06:30:00Z",
completedAt: "2025-12-20T11:00:00Z",
score: 82
}
}
CRM Actions:
- Create
TEETIME_COMPLETEDactivity - Trigger engagement score refresh
teetime.booking.cancelled
Fired when a booking is cancelled.
{
type: "teetime.booking.cancelled",
identity: { teetimePlayerId: "player_uuid_123" },
payload: {
id: "booking_789",
reason: "weather",
cancelledAt: "2025-12-19T18:00:00Z"
}
}
CRM Actions:
- Create
TEETIME_CANCELLEDactivity
teetime.booking.noshow
Fired when player doesn't show up.
{
type: "teetime.booking.noshow",
identity: { teetimePlayerId: "player_uuid_123" },
payload: {
id: "booking_789",
scheduledTime: "2025-12-20T06:30:00Z"
}
}
CRM Actions:
- Create
TEETIME_NOSHOWactivity - Consider for churn risk
teetime.competition.entered
Fired when player enters a competition.
{
type: "teetime.competition.entered",
identity: { teetimePlayerId: "player_uuid_123" },
payload: {
competitionId: "comp_456",
competitionName: "Club Championship 2025",
playerId: "player_uuid_123",
entryDate: "2025-12-14T10:00:00Z",
competitionDate: "2025-12-28T07:00:00Z"
}
}
CRM Actions:
- Create
COMPETITION_ENTEREDactivity
Messaging Service Events
The crm-events queue uses the job name as the event type. Examples include a type field for readability, while the payload matches the job data.payload.
contact.created
Fired when a new contact is created.
{
type: "contact.created",
payload: {
eventId: "evt_123",
tenantId: "tenant_123",
contactId: "contact_uuid_456",
email: "john@example.com",
phoneNumber: "+27821234567",
firstName: "John",
lastName: "Smith",
emailOptIn: true,
smsOptIn: false
}
}
CRM Actions:
- Resolve identity
- Link
messagingContactId - Sync consent flags
contact.updated
Fired when contact details change.
{
type: "contact.updated",
payload: {
eventId: "evt_124",
tenantId: "tenant_123",
contactId: "contact_uuid_456",
email: "john@example.com",
phoneNumber: "+27821234567",
firstName: "Jonathan",
lastName: "Smith-Jones",
emailOptIn: true,
smsOptIn: false
}
}
contactPreference.updated
Fired when consent/opt-in status changes.
{
type: "contactPreference.updated",
payload: {
eventId: "evt_125",
tenantId: "tenant_123",
contactId: "contact_uuid_456",
smsOptIn: true
}
}
CRM Actions:
- Update consent flags on profile (
smsOptIn, etc.) - Create/update
MarketingConsentrecord - Create
CONSENT_OPTED_INorCONSENT_OPTED_OUTactivity
message.sent
Fired when a message is sent.
{
type: "message.sent",
identity: { messagingContactId: "contact_uuid_456" },
payload: {
messageId: "msg_789",
contactId: "contact_uuid_456",
channel: "EMAIL",
templateId: "template_123",
templateName: "December Newsletter",
campaignId: "campaign_dec_2025",
sentAt: "2025-12-14T08:00:00Z"
}
}
CRM Actions:
- Create
MESSAGE_SENTactivity - Create/update
CampaignInteraction(type: SENT) - Update
lastMessageAt
message.delivered
Fired when delivery is confirmed.
{
type: "message.delivered",
identity: { messagingContactId: "contact_uuid_456" },
payload: {
messageId: "msg_789",
deliveredAt: "2025-12-14T08:00:05Z"
}
}
CRM Actions:
- Create
MESSAGE_DELIVEREDactivity - Update
CampaignInteraction(type: DELIVERED)
message.failed
Fired when delivery fails.
{
type: "message.failed",
identity: { messagingContactId: "contact_uuid_456" },
payload: {
messageId: "msg_789",
reason: "invalid_email",
failedAt: "2025-12-14T08:00:05Z"
}
}
CRM Actions:
- Create
MESSAGE_FAILEDactivity - Consider marking email/phone as unverified
message.bounced
Fired when email bounces.
{
type: "message.bounced",
identity: { messagingContactId: "contact_uuid_456" },
payload: {
messageId: "msg_789",
bounceType: "hard", // hard or soft
reason: "mailbox_not_found",
bouncedAt: "2025-12-14T08:00:10Z"
}
}
CRM Actions:
- Create
MESSAGE_BOUNCEDactivity - If hard bounce, set
emailVerified= false - Auto opt-out from email if multiple hard bounces
email.opened
Fired when email is opened (tracking pixel).
{
type: "email.opened",
identity: { messagingContactId: "contact_uuid_456" },
payload: {
messageId: "msg_789",
campaignId: "campaign_dec_2025",
openedAt: "2025-12-14T09:15:00Z",
deviceType: "mobile",
userAgent: "..."
}
}
CRM Actions:
- Create
EMAIL_OPENEDactivity - Create
CampaignInteraction(type: OPENED) - Update
lastActivityAt - Trigger engagement score refresh
email.clicked
Fired when a link in email is clicked.
{
type: "email.clicked",
identity: { messagingContactId: "contact_uuid_456" },
payload: {
messageId: "msg_789",
campaignId: "campaign_dec_2025",
linkId: "link_123",
linkUrl: "https://example.com/offer",
clickedAt: "2025-12-14T09:16:30Z"
}
}
CRM Actions:
- Create
EMAIL_CLICKEDactivity - Create
CampaignInteraction(type: CLICKED) - Create
AttributionEventif applicable
sms.replied
Fired when customer replies to SMS.
{
type: "sms.replied",
identity: { messagingContactId: "contact_uuid_456" },
payload: {
originalMessageId: "msg_789",
replyText: "Yes, I'm interested",
repliedAt: "2025-12-14T10:00:00Z"
}
}
CRM Actions:
- Create
SMS_REPLIEDactivity - High-value engagement indicator
Feedback Service Events
feedback.voted
Fired when a user allocates credits to vote on a feature.
{
type: "feedback.voted",
tenantId: "tenant_123",
identity: {
authUserId: "auth0|123",
email: "john@example.com"
},
payload: {
featureId: "feature_uuid_456",
featureTitle: "Dark mode support",
credits: 5,
totalVotes: 15,
votedAt: "2026-01-15T10:00:00Z"
}
}
CRM Actions:
- Resolve identity → find profile
- Create
FEEDBACK_VOTEDactivity with feature details - Update engagement indicators
feedback.unvoted
Fired when a user removes their vote from a feature.
{
type: "feedback.unvoted",
tenantId: "tenant_123",
identity: {
authUserId: "auth0|123",
email: "john@example.com"
},
payload: {
featureId: "feature_uuid_456",
featureTitle: "Dark mode support",
creditsReturned: 5,
unvotedAt: "2026-01-16T08:00:00Z"
}
}
CRM Actions:
- Resolve identity → find profile
- Create
FEEDBACK_UNVOTEDactivity
feedback.shipped
Fired when a feature is marked as shipped (credits auto-returned to voters).
{
type: "feedback.shipped",
tenantId: "tenant_123",
identity: {
authUserId: "auth0|123",
email: "john@example.com"
},
payload: {
featureId: "feature_uuid_456",
featureTitle: "Dark mode support",
creditsReturned: 5,
shippedAt: "2026-01-20T14:00:00Z"
}
}
CRM Actions:
- Resolve identity → find profile
- Create
FEATURE_SHIPPEDactivity - Potential journey trigger for feature announcements
Events Produced
The CRM service produces events for downstream consumers.
crm.profile.created
Fired when a new CustomerProfile is created.
{
type: "crm.profile.created",
tenantId: "tenant_123",
payload: {
id: "profile_uuid_789",
email: "john@example.com",
firstName: "John",
lastName: "Smith",
source: "scl.member.created",
createdAt: "2025-12-14T10:00:00Z"
}
}
crm.profile.updated
Fired when profile is updated.
{
type: "crm.profile.updated",
tenantId: "tenant_123",
payload: {
id: "profile_uuid_789",
changes: ["engagementScore", "churnRiskScore"],
engagementScore: 75,
churnRiskScore: 15,
updatedAt: "2025-12-14T10:00:00Z"
}
}
crm.profile.merged
Fired when two profiles are merged.
{
type: "crm.profile.merged",
tenantId: "tenant_123",
payload: {
winnerId: "profile_uuid_789",
loserId: "profile_uuid_790",
mergedBy: "user_123",
mergedAt: "2025-12-14T10:00:00Z"
}
}
crm.segment.membership.changed
Fired when segment membership changes.
{
type: "crm.segment.membership.changed",
tenantId: "tenant_123",
payload: {
segmentId: "segment_123",
segmentName: "High-Value Members",
profileId: "profile_uuid_789",
action: "JOINED", // or "LEFT"
reason: "engagementScore increased to 75",
occurredAt: "2025-12-14T10:00:00Z"
}
}
crm.churn.risk.high
Fired when customer's churn risk exceeds threshold.
{
type: "crm.churn.risk.high",
tenantId: "tenant_123",
payload: {
profileId: "profile_uuid_789",
customerName: "John Smith",
email: "john@example.com",
churnRiskScore: 85,
riskFactors: [
"No activity in 45 days",
"Membership expiring in 15 days",
"Engagement score dropped 30%"
],
detectedAt: "2025-12-14T02:00:00Z"
}
}
Event Processing Configuration
Queue Configuration
// BullMQ queue settings
const queueConfig = {
name: 'crm-events',
defaultJobOptions: {
attempts: 3,
backoff: {
type: 'exponential',
delay: 1000, // 1s, 2s, 4s
},
removeOnComplete: 1000,
removeOnFail: 5000,
},
};
Dead Letter Queue
Failed events after all retries go to DLQ for manual review.
const dlqConfig = {
name: 'crm-events-dlq',
// Events stay for 7 days before auto-delete
removeOnComplete: {
age: 7 * 24 * 60 * 60 * 1000,
},
};
Event Priorities
| Priority | Events | Processing |
|---|---|---|
| High | contactPreference.updated, payment.*, message.bounced | Immediate |
| Normal | member., player., booking.* | Within seconds |
| Low | email.opened, email.clicked | Batched |
Idempotency
Events are processed idempotently using:
- Event ID: Each event has unique ID
- SyncLog: Records processed events
- Deduplication: Skip if event ID already processed
async processEvent(event: CrmEvent) {
// Check if already processed
const existing = await this.syncLog.findByEventId(event.id);
if (existing?.status === 'COMPLETED') {
return; // Skip duplicate
}
// Process and record
await this.syncLog.create({
eventId: event.id,
eventType: event.type,
status: 'PROCESSING',
});
try {
await this.handleEvent(event);
await this.syncLog.update(event.id, { status: 'COMPLETED' });
} catch (error) {
await this.syncLog.update(event.id, { status: 'FAILED', error: error.message });
throw error;
}
}
Monitoring & Alerts
Metrics
| Metric | Description |
|---|---|
crm_events_processed_total | Total events processed by type |
crm_events_failed_total | Failed events by type |
crm_event_processing_duration | Processing time histogram |
crm_event_queue_depth | Current queue depth |
crm_identity_resolution_rate | % events matched to existing profiles |
Alerts
| Alert | Condition | Severity |
|---|---|---|
| High queue depth | Queue > 10,000 | Warning |
| Processing failures | Failure rate > 5% | Critical |
| DLQ growth | DLQ > 100 events | Warning |
| Latency spike | p99 > 10s | Warning |
Appendix: Full Event Type List
Consumed Events
| Source | Event Type | Priority |
|---|---|---|
| SCL | scl.member.created | Normal |
| SCL | scl.member.updated | Normal |
| SCL | scl.member.deleted | Normal |
| SCL | scl.member.tier.changed | Normal |
| SCL | scl.member.status.changed | Normal |
| SCL | scl.payment.received | Normal |
| SCL | scl.payment.failed | High |
| SCL | scl.invoice.sent | Low |
| TeeTime | teetime.player.created | Normal |
| TeeTime | teetime.player.updated | Normal |
| TeeTime | teetime.handicap.updated | Normal |
| TeeTime | teetime.booking.created | Normal |
| TeeTime | teetime.booking.completed | Normal |
| TeeTime | teetime.booking.cancelled | Normal |
| TeeTime | teetime.booking.noshow | Normal |
| TeeTime | teetime.competition.entered | Low |
| TeeTime | teetime.competition.completed | Low |
| TeeTime | teetime.score.posted | Low |
| Messaging | contact.created | Normal |
| Messaging | contact.updated | Normal |
| Messaging | contactPreference.updated | High |
| Messaging | message.sent | Normal |
| Messaging | message.delivered | Low |
| Messaging | message.failed | High |
| Messaging | message.bounced | High |
| Messaging | email.opened | Low |
| Messaging | email.clicked | Low |
| Messaging | sms.replied | Normal |
| Feedback | feedback.voted | Normal |
| Feedback | feedback.unvoted | Normal |
| Feedback | feedback.shipped | Low |
Produced Events
| Event Type | Trigger |
|---|---|
crm.profile.created | New profile created |
crm.profile.updated | Profile fields changed |
crm.profile.merged | Two profiles merged |
crm.profile.deleted | Profile soft-deleted |
crm.segment.membership.changed | Customer joins/leaves segment |
crm.engagement.score.changed | Score threshold crossed |
crm.churn.risk.high | Churn risk > 70 |
crm.churn.risk.critical | Churn risk > 90 |