Skip to main content

Block Notifications

The block notification system automatically notifies players when tee sheet blocks (maintenance, competitions, manual) affect their bookings.

Architecture

┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
│ Block Created │────▶│ BlockNotification│────▶│ Affected │
│ /Updated │ │ Service │ │ Detection │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│ │
▼ ▼
┌──────────────────┐ ┌─────────────────┐
│ Per-Course │ │ Bulk Notify │
│ Settings Check │ │ via Queue │
└──────────────────┘ └─────────────────┘

Source: libs/tee-time-services/src/lib/blocks/block-notification.service.ts

Block Types

TypeDescriptionUse Case
MAINTENANCECourse maintenanceAeration, repairs
COMPETITIONTournament/eventCompetitions, outings
MANUALAdmin-createdAd-hoc closures

Recurrence Types

TypeConfigurationExample
NONESingle occurrenceOne-time maintenance
WEEKLYSpecific weekdays["MON", "WED", "FRI"]
MONTHLYDay of month15 (15th of each month)

Recurrence Fields

{
recurrenceType: 'WEEKLY',
recurrenceDays: ['MON', 'WED', 'FRI'], // For WEEKLY
recurrenceDayOfMonth: 15, // For MONTHLY
recurrenceEndDate: '2025-12-31' // Bounds the recurrence
}

Notification Flow

1. Block Creation Trigger

// POST /v1/courses/:courseId/blocks
{
"name": "Greens Maintenance",
"type": "MAINTENANCE",
"startDate": "2025-12-15",
"endDate": "2025-12-15",
"startTime": "06:00",
"endTime": "12:00",
"notifyPlayers": true // Optional override
}

2. Notification Decision

async shouldNotify(block, override?: boolean): Promise<boolean> {
// Override takes precedence
if (typeof override === 'boolean') return override;

// Query course tee-sheet config
const config = await courseTeeSheetRepo.findOne({
where: {
courseId: block.courseId,
startDate: { lte: block.startDate },
endDate: { gte: block.endDate }
}
});

return config?.notifyPlayersOnBlocks ?? false; // Default: disabled
}

3. Occurrence Expansion

For recurring blocks, all occurrences are calculated:

// WEEKLY example: Mon, Wed, Fri from Dec 1-31
// Generates: Dec 2, 4, 6, 9, 11, 13, ... (all matching weekdays)

function expandOccurrences(block): Date[] {
const occurrences: Date[] = [];
let cursor = block.startDate;
const endBound = min(block.recurrenceEndDate, block.endDate);

while (cursor <= endBound) {
if (matchesRecurrence(cursor, block)) {
occurrences.push(cursor);
}
cursor = addDays(cursor, 1);
}

return occurrences;
}

4. Affected Booking Detection

// Find all booked slots within block window
const affectedSlots = await slotRepo.findBookedSlotsForWindow({
courseId: block.courseId,
windowStart: blockStartDateTime,
windowEnd: blockEndDateTime,
tees: block.tees // Optional: specific tee numbers
});

Slot Query Criteria:

  • Status: BOOKED
  • Time: Within block window
  • Tees: Matching affected tee numbers (if specified)

5. Recipient Filtering

// Filter to valid recipients
const recipients = affectedSlots
.filter(slot => slot.booking?.status !== 'CANCELLED')
.filter(slot => slot.booking?.contactEmail || slot.booking?.contactPhone)
.map(slot => ({
playerId: slot.booking?.players?.[0]?.id ?? slot.booking?.id,
email: slot.booking?.contactEmail,
phoneNumber: slot.booking?.contactPhone
}));

// Deduplicate by player
const uniqueRecipients = deduplicateByPlayerId(recipients);

6. Bulk Notification

// Send via messaging queue
await messagingService.sendBulkNotice(recipients, {
tenantId,
clubId,
courseId,
courseName,
subject: `Tee sheet block: ${block.name}`,
message: block.description ?? 'A tee sheet block affects your booking',
targetDate: block.startDate,
affectedBookingCount: recipients.length,
blockId: block.id,
blockName: block.name,
blockReason: block.description,
windowStart: block.startDateTime,
windowEnd: block.endDateTime,
tees: block.tees
});

Per-Course Configuration

Enable Notifications

// PUT /admin/courses/:courseId/tee-sheet/config
{
"notifyPlayersOnBlocks": true
}

Configuration Location

  • Stored in CourseTeeSheet record
  • Field: notifyPlayersOnBlocks (default: false)
  • Applies to all blocks for that course

Override at Block Level

// Force notification regardless of course setting
POST /v1/courses/:courseId/blocks
{
"name": "Emergency Closure",
"notifyPlayers": true // Overrides course setting
}

Channel Priority

Notifications are sent via configured channels:

const CHANNEL_PRIORITY = ['WHATSAPP', 'PUSH', 'IN_APP', 'SMS', 'EMAIL'];

Template: BULK_NOTICE

API Endpoints

MethodEndpointDescription
POST/v1/courses/:courseId/blocksCreate block (triggers notification)
PUT/v1/courses/:courseId/blocks/:idUpdate block (triggers notification)
DELETE/v1/courses/:courseId/blocks/:idDelete block

Notification Data

interface BlockNotificationData {
tenantId: string;
clubId: string;
courseId: string;
courseName: string;
subject: string;
message: string;
targetDate: string; // ISO date of block
affectedBookingCount: number;
blockId: string;
blockName: string;
blockReason?: string;
windowStart: string; // ISO datetime
windowEnd: string; // ISO datetime
tees: number[]; // Affected tee numbers
}

Error Handling

ScenarioBehavior
Queue unavailableLog warning, skip notification
No affected bookingsNo notifications sent
Invalid contact infoPlayer skipped
Course setting disabledNo notifications (unless override)

Architecture Notes

  1. Decoupled — Block creation doesn't wait for notification delivery
  2. Graceful Degradation — Continues if messaging queue unavailable
  3. Deduplication — Players with multiple bookings receive one notification
  4. Bulk Efficiency — All recipients notified in single queue operation

Troubleshooting

Notifications Not Sending

  1. Check notifyPlayersOnBlocks is enabled for course
  2. Verify messaging queue is available
  3. Check bookings have valid contact info (email/phone)
  4. Confirm block hasn't been created with notifyPlayers: false

Duplicate Notifications

  1. Check for overlapping recurring blocks
  2. Verify deduplication by playerId is working
  3. Review block update vs create calls

Missing Recipients

  1. Verify bookings have status !== 'CANCELLED'
  2. Check contact email or phone is populated
  3. Confirm slot is within block time window