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
| Type | Description | Use Case |
|---|---|---|
MAINTENANCE | Course maintenance | Aeration, repairs |
COMPETITION | Tournament/event | Competitions, outings |
MANUAL | Admin-created | Ad-hoc closures |
Recurrence Types
| Type | Configuration | Example |
|---|---|---|
NONE | Single occurrence | One-time maintenance |
WEEKLY | Specific weekdays | ["MON", "WED", "FRI"] |
MONTHLY | Day of month | 15 (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
CourseTeeSheetrecord - 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
| Method | Endpoint | Description |
|---|---|---|
POST | /v1/courses/:courseId/blocks | Create block (triggers notification) |
PUT | /v1/courses/:courseId/blocks/:id | Update block (triggers notification) |
DELETE | /v1/courses/:courseId/blocks/:id | Delete 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
| Scenario | Behavior |
|---|---|
| Queue unavailable | Log warning, skip notification |
| No affected bookings | No notifications sent |
| Invalid contact info | Player skipped |
| Course setting disabled | No notifications (unless override) |
Architecture Notes
- Decoupled — Block creation doesn't wait for notification delivery
- Graceful Degradation — Continues if messaging queue unavailable
- Deduplication — Players with multiple bookings receive one notification
- Bulk Efficiency — All recipients notified in single queue operation
Troubleshooting
Notifications Not Sending
- Check
notifyPlayersOnBlocksis enabled for course - Verify messaging queue is available
- Check bookings have valid contact info (email/phone)
- Confirm block hasn't been created with
notifyPlayers: false
Duplicate Notifications
- Check for overlapping recurring blocks
- Verify deduplication by playerId is working
- Review block update vs create calls
Missing Recipients
- Verify bookings have status !== 'CANCELLED'
- Check contact email or phone is populated
- Confirm slot is within block time window