Skip to main content

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:

  1. Resolve identity → find or create CustomerProfile
  2. Link sclMemberId to profile
  3. Sync member data (firstName, lastName, etc.)
  4. Create MEMBER_CREATED activity
  5. Set lifecycleStage to CUSTOMER

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:

  1. Find profile by sclMemberId
  2. Update changed fields
  3. Create MEMBER_UPDATED activity
  4. 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:

  1. Update membershipTier on profile
  2. Create TIER_UPGRADED or TIER_DOWNGRADED activity
  3. Trigger engagement score refresh
  4. 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:

  1. Update membershipStatus on profile
  2. If INACTIVE → update lifecycleStage to LAPSED
  3. Create MEMBERSHIP_SUSPENDED or MEMBERSHIP_CANCELLED activity
  4. 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:

  1. Increment lifetimeValue on profile
  2. Update lastPaymentAt
  3. Create PAYMENT_RECEIVED activity
  4. 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:

  1. Create PAYMENT_FAILED activity
  2. 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:

  1. Resolve identity → find or create CustomerProfile
  2. Link teetimePlayerId to profile
  3. Sync player data (handicap, homeClub)
  4. Create PROFILE_CREATED activity 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:

  1. Update handicap and handicapUpdatedAt
  2. Create HANDICAP_UPDATED activity

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:

  1. Create TEETIME_BOOKED activity
  2. Increment bookingCount30d, bookingCountTotal
  3. 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:

  1. Create TEETIME_COMPLETED activity
  2. 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:

  1. Create TEETIME_CANCELLED activity

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:

  1. Create TEETIME_NOSHOW activity
  2. 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:

  1. Create COMPETITION_ENTERED activity

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:

  1. Resolve identity
  2. Link messagingContactId
  3. 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:

  1. Update consent flags on profile (smsOptIn, etc.)
  2. Create/update MarketingConsent record
  3. Create CONSENT_OPTED_IN or CONSENT_OPTED_OUT activity

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:

  1. Create MESSAGE_SENT activity
  2. Create/update CampaignInteraction (type: SENT)
  3. 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:

  1. Create MESSAGE_DELIVERED activity
  2. 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:

  1. Create MESSAGE_FAILED activity
  2. 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:

  1. Create MESSAGE_BOUNCED activity
  2. If hard bounce, set emailVerified = false
  3. 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:

  1. Create EMAIL_OPENED activity
  2. Create CampaignInteraction (type: OPENED)
  3. Update lastActivityAt
  4. 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:

  1. Create EMAIL_CLICKED activity
  2. Create CampaignInteraction (type: CLICKED)
  3. Create AttributionEvent if 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:

  1. Create SMS_REPLIED activity
  2. 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:

  1. Resolve identity → find profile
  2. Create FEEDBACK_VOTED activity with feature details
  3. 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:

  1. Resolve identity → find profile
  2. Create FEEDBACK_UNVOTED activity

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:

  1. Resolve identity → find profile
  2. Create FEATURE_SHIPPED activity
  3. 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

PriorityEventsProcessing
HighcontactPreference.updated, payment.*, message.bouncedImmediate
Normalmember., player., booking.*Within seconds
Lowemail.opened, email.clickedBatched

Idempotency

Events are processed idempotently using:

  1. Event ID: Each event has unique ID
  2. SyncLog: Records processed events
  3. 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

MetricDescription
crm_events_processed_totalTotal events processed by type
crm_events_failed_totalFailed events by type
crm_event_processing_durationProcessing time histogram
crm_event_queue_depthCurrent queue depth
crm_identity_resolution_rate% events matched to existing profiles

Alerts

AlertConditionSeverity
High queue depthQueue > 10,000Warning
Processing failuresFailure rate > 5%Critical
DLQ growthDLQ > 100 eventsWarning
Latency spikep99 > 10sWarning

Appendix: Full Event Type List

Consumed Events

SourceEvent TypePriority
SCLscl.member.createdNormal
SCLscl.member.updatedNormal
SCLscl.member.deletedNormal
SCLscl.member.tier.changedNormal
SCLscl.member.status.changedNormal
SCLscl.payment.receivedNormal
SCLscl.payment.failedHigh
SCLscl.invoice.sentLow
TeeTimeteetime.player.createdNormal
TeeTimeteetime.player.updatedNormal
TeeTimeteetime.handicap.updatedNormal
TeeTimeteetime.booking.createdNormal
TeeTimeteetime.booking.completedNormal
TeeTimeteetime.booking.cancelledNormal
TeeTimeteetime.booking.noshowNormal
TeeTimeteetime.competition.enteredLow
TeeTimeteetime.competition.completedLow
TeeTimeteetime.score.postedLow
Messagingcontact.createdNormal
Messagingcontact.updatedNormal
MessagingcontactPreference.updatedHigh
Messagingmessage.sentNormal
Messagingmessage.deliveredLow
Messagingmessage.failedHigh
Messagingmessage.bouncedHigh
Messagingemail.openedLow
Messagingemail.clickedLow
Messagingsms.repliedNormal
Feedbackfeedback.votedNormal
Feedbackfeedback.unvotedNormal
Feedbackfeedback.shippedLow

Produced Events

Event TypeTrigger
crm.profile.createdNew profile created
crm.profile.updatedProfile fields changed
crm.profile.mergedTwo profiles merged
crm.profile.deletedProfile soft-deleted
crm.segment.membership.changedCustomer joins/leaves segment
crm.engagement.score.changedScore threshold crossed
crm.churn.risk.highChurn risk > 70
crm.churn.risk.criticalChurn risk > 90