Skip to main content

TeeTime Tee Sheet Management — API & Data Models

Status: Draft Owners: TeeTime Eng Last Updated: 2025-12-11 Parent Document: tee-sheet-management-spec.md


At a Glance

  • Surface families: Admin (courses, tee-sheets, rules, blocks), Weather, Carts/Items, Waitlist, Analytics, Communication.
  • Auth: Admin JWT with teetime-admin audience; role-scoped by Access Control.
  • Transport: REST JSON; WebSocket for live updates (see wireframes for signals).
  • Status tags: Available = shipped endpoints; Required = backlog.
  • Pagination: page, limit (default 20/50); X-Total-Count on list endpoints.
  • Time formats: Dates ISO (2025-12-25); Times HH:mm or ISO timestamps where noted.
  • Idempotency: externalRef for booking-adjacent writes; see provider-specific notes.

Status Legend & Conventions

TagMeaning
✅ AvailableImplemented/shipping
🧭 RequiredBacklog item
  • Headers: Authorization: Bearer <token> (audience teetime-admin), Content-Type: application/json.
  • Errors: JSON { error: string; message: string; statusCode: number; details?: Record<string, unknown> }.
  • Pagination: ?page=1&limit=50; totals in X-Total-Count.
  • Idempotency: supply externalRef on create/update where supported.

Example error:

{
"error": "Bad Request",
"message": "End time must be after start time",
"statusCode": 400,
"details": { "field": "endTime" }
}

Endpoints

Available (backend implemented)

StatusMethodPathDescription
GET/admin/clubsList clubs for selector
GET/admin/clubs/:id/coursesList courses for a club
GET/admin/courses/:id/tee-sheet/:dateDaily tee sheet with slots, rates, and stats
GET/admin/courses/:id/tee-sheet/settingsGet tee sheet configuration
PUT/admin/courses/:id/tee-sheet/settingsUpdate tee sheet configuration
PATCH/admin/tee-sheets/:id/offlineSet online/offline status
GET/admin/courses/:id/blocksList blocks for a course
POST/admin/courses/:id/blocksCreate a block (single or recurring)
PUT/admin/courses/:id/blocks/:idUpdate a block
DELETE/admin/courses/:id/blocks/:idDelete a block
GET/admin/courses/:id/rules/memberList member booking rules
POST/admin/courses/:id/rules/memberCreate member rule
PUT/admin/courses/:id/rules/member/:idUpdate member rule
DELETE/admin/courses/:id/rules/member/:idDelete member rule
GET/admin/courses/:id/rules/rateList rate rules
POST/admin/courses/:id/rules/rateCreate rate rule
PUT/admin/courses/:id/rules/rate/:idUpdate rate rule
DELETE/admin/courses/:id/rules/rate/:idDelete rate rule
GET/admin/courses/:id/starter-sheetGenerate starter sheet PDF
FieldsuseFacilitiesCarts + facilitiesClubId on courses enable Facilities-powered carts (tee-time carts become read-only; items stay tee-time native)
FieldsweatherThresholds (wind/precip/playability + auto-block) persisted with tee-sheet settings

Required / backlog

StatusMethodPathDescription
🧭GET/admin/courses/:id/tee-timesDedicated tee-time search with filters (current daily view endpoint is used instead)
🧭GET/admin/courses/:id/tee-times/:uuidGet single tee time details
🧭POST/admin/courses/:id/tee-times/:uuid/blockBlock a tee time
🧭DELETE/admin/courses/:id/tee-times/:uuid/blockUnblock a tee time
🧭GET/admin/courses/:id/tee-sheet/:date/statsGet daily statistics

Weather Endpoints

Status: ✅ available unless noted.

StatusMethodPathDescription
GET/v1/courses/:id/weatherCourse-scoped snapshot with playability score, hourly projection, auto-block on severe
GET/weather/forecast7-day weather forecast (lat/lon or free-text location)
GET/weather/currentCurrent conditions (lat/lon or free-text location)
GET/weather/hourlyHourly forecast for a date
GET/weather/courses/:id/forecastCourse forecast using stored coordinates
🧭PUT/admin/clubs/:id/weather/settingsConfigure weather thresholds (planned admin surface)

Cart & Equipment Endpoints

Status: ✅ Available via CourseInventoryController at /v1/courses/:courseId/*. When useFacilitiesCarts is enabled, carts/reservations proxy to Facilities (read-only in tee-time); items always from tee-time.

StatusMethodPathDescription
GET/v1/courses/:courseId/cartsList cart inventory
POST/v1/courses/:courseId/cartsAdd cart to inventory
PUT/v1/courses/:courseId/carts/:idUpdate cart details
DELETE/v1/courses/:courseId/carts/:idRemove cart from inventory
GET/v1/courses/:courseId/cart-reservationsList reservations by date (?date=YYYY-MM-DD)
🧭GET/v1/courses/:courseId/carts/availability/:dateGet cart availability summary for date
🧭POST/v1/courses/:courseId/carts/reserveReserve cart for booking
🧭DELETE/v1/courses/:courseId/cart-reservations/:idCancel cart reservation
🧭POST/DELETE/bookings/:id/cart-assignmentPlanned: assign/release cart for booking when Facilities carts are enabled

Additional Items Endpoints

Status: ✅ Available via CourseInventoryController. Items always managed in tee-time (not proxied to Facilities).

StatusMethodPathDescription
GET/v1/courses/:courseId/itemsList bookable additional items
POST/v1/courses/:courseId/itemsCreate additional item
PUT/v1/courses/:courseId/items/:idUpdate item details/pricing (auto-updates status based on stock)
DELETE/v1/courses/:courseId/items/:idRemove item
🧭PATCH/v1/courses/:courseId/items/:id/availabilityToggle item availability
🧭GET/v1/courses/:courseId/items/categoriesList item categories
🧭POST/v1/courses/:courseId/items/categoriesCreate item category

Facilities Hybrid Notes

  • Enable via Course fields: useFacilitiesCarts + facilitiesClubId + facilitiesTenantId.
  • When enabled: carts/reservations proxy to Facilities (read-only carts in tee-time); items stay tee-time native.
  • Booking hooks deferred: Requires cross-module schema changes:
    1. Tee-time bookings use UUID; Facilities expects numeric bookingId
    2. Need facilitiesAssignmentId field on tee-time Booking model
    3. Need Facilities findActiveAssignmentByBookingId + returnCartByBookingId methods
    4. Cart selection not yet captured in booking flow
  • Decision: Defer to future work; document required schema changes; proceed with P2 phases.

Real-time Infrastructure

Current: Server-Sent Events (SSE) via /clubs/tee-sheet/stream for multi-club streaming.
Live: WebSocket gateway with room-based subscriptions for live slot updates.

StatusMethodPathDescription
GET/clubs/tee-sheet/streamSSE stream for multi-club tee sheet data
WS/ws/tee-sheetWebSocket for live slot/booking/check-in/pace events (rooms per club/course/date/booking)

Check-in Endpoints

Status: ✅ available (Phase 7 shipped).

StatusMethodPathDescription
POST/admin/bookings/:id/check-inCheck in a player
POST/admin/bookings/:id/check-in/qrCheck in via QR code scan
DELETE/admin/bookings/:id/check-inUndo check-in
GET/admin/courses/:id/check-ins/:dateGet all check-ins for date
POST/admin/courses/:id/starter-callAnnounce tee time ready

Pace of Play Endpoints

Status: ✅ available (Phase 7 shipped).

StatusMethodPathDescription
GET/admin/courses/:id/pace/:dateGet pace of play for date
POST/admin/bookings/:id/tee-offRecord tee-off time
POST/admin/bookings/:id/hole-completeRecord hole completion
POST/admin/bookings/:id/round-completeRecord round completion
GET/admin/courses/:id/pace/alertsGet slow play alerts
POST/admin/courses/:id/pace/alert/:groupIdSend pace warning to group

Waitlist Endpoints

Status: ✅ Available via WaitlistController at /admin/courses/:courseId/waitlist and WaitlistAdminController at /admin/waitlist.

Core Waitlist Operations (WaitlistController):

StatusMethodPathDescription
GET/admin/courses/:id/waitlist/:dateGet waitlist entries for date
POST/admin/courses/:id/waitlistJoin waitlist for a slot
DELETE/admin/courses/:id/waitlist/:tenantId/:slotId/:memberIdLeave waitlist
GET/admin/courses/:id/waitlist/position/:slotId/:memberIdGet queue position
GET/admin/courses/:id/waitlist/count/:slotIdGet waitlist count for slot
GET/admin/courses/:id/waitlist/member/:tenantId/:memberIdGet member's active waitlists
POST/admin/courses/:id/waitlist/:entryId/offerManually offer slot to entry
POST/admin/courses/:id/waitlist/:entryId/acceptAccept offered slot
POST/admin/courses/:id/waitlist/:entryId/bookedMark entry as booked (internal)

Admin Settings & Analytics (WaitlistAdminController):

StatusMethodPathDescription
GET/admin/waitlist/courses/:courseId/settingsGet per-course waitlist settings
PUT/admin/waitlist/courses/:courseId/settingsUpdate per-course waitlist settings
DELETE/admin/waitlist/courses/:courseId/settingsReset settings to defaults
GET/admin/waitlist/courses/:courseId/analyticsGet waitlist performance analytics
POST/admin/waitlist/courses/:courseId/expire-staleBulk expire stale entries
POST/admin/waitlist/courses/:courseId/cascadeCascade offers on cancellation

Database Models:

  • WaitlistEntry - Waitlist queue entries with FIFO ordering
  • WaitlistOffer - Persistent offer tracking with status (PENDING, ACCEPTED, DECLINED, EXPIRED)
  • CourseWaitlistSettings - Per-course configuration with defaults fallback

Player Intelligence Endpoints

Status: 🧭 planned unless noted.

StatusMethodPathDescription
🧭GET/admin/players/:id/profileGet full player profile
🧭GET/admin/players/:id/historyGet booking history
🧭GET/admin/players/:id/statsGet player statistics
🧭GET/admin/players/:id/no-showsGet no-show history
🧭POST/admin/players/:id/flagsAdd player flag (VIP, Warning)
🧭DELETE/admin/players/:id/flags/:flagIdRemove player flag
🧭GET/admin/players/:id/preferencesGet player preferences
🧭PUT/admin/players/:id/preferencesUpdate preferences

Communication Endpoints

Status: Partially available via TeeTimeMessagingService (library-level, not REST).

Event-Driven Messaging (✅ Available - internal services):

StatusService MethodDescription
sendBookingConfirmation()Send booking confirmation to player
sendBookingReminder()Send booking reminder (method exists, scheduler pending)
sendBookingCancelled()Send cancellation notice
sendCheckInConfirmation()Send check-in confirmation
sendPaceAlert()Send pace of play alert
sendWeatherAlert()Send weather alert
sendPlayerMessage()Direct message to player
sendBulkNotice()Course-wide announcement
sendBookingReminderBulk()Bulk booking reminders
sendWeatherAlertBulk()Bulk weather alerts

REST Endpoints (🧭 planned):

StatusMethodPathDescription
🧭POST/admin/bookings/:id/notifySend notification to player
🧭POST/admin/tee-times/:uuid/notify-allNotify entire fourball
🧭GET/admin/courses/:id/notifications/templatesList notification templates
🧭PUT/admin/courses/:id/notifications/templates/:idUpdate template
🧭GET/admin/courses/:id/notifications/settingsGet notification settings
🧭PUT/admin/courses/:id/notifications/settingsUpdate settings
🧭POST/admin/courses/:id/notifications/bulkSend bulk notification

Multi-Channel Support:

  • WhatsApp (Meta Business API)
  • SMS (Twilio, SMSPortal)
  • Email (SendGrid, SMTP2GO)
  • Push (device token management)
  • Priority: WhatsApp > Push > SMS > Email

Tournament & Group Endpoints

Status: 🧭 planned unless noted.

StatusMethodPathDescription
🧭GET/admin/courses/:id/tournamentsList tournaments
🧭POST/admin/courses/:id/tournamentsCreate tournament
🧭GET/admin/courses/:id/tournaments/:tournamentIdGet tournament details
🧭PUT/admin/courses/:id/tournaments/:tournamentIdUpdate tournament
🧭DELETE/admin/courses/:id/tournaments/:tournamentIdDelete tournament
🧭POST/admin/courses/:id/tournaments/:tournamentId/drawGenerate draw
🧭GET/admin/courses/:id/tournaments/:tournamentId/flightsGet flights
🧭POST/admin/courses/:id/tournaments/:tournamentId/flightsCreate/update flights
🧭POST/admin/courses/:id/group-bookingCreate group/corporate booking
🧭GET/admin/courses/:id/group-bookingsList group bookings

Analytics Endpoints

Status: 🧭 planned unless noted.

StatusMethodPathDescription
🧭GET/admin/courses/:id/analytics/demandGet demand heatmap data
🧭GET/admin/courses/:id/analytics/revenueGet revenue analytics
🧭GET/admin/courses/:id/analytics/utilizationGet utilization trends
🧭GET/admin/courses/:id/analytics/cancellationsGet cancellation patterns
🧭GET/admin/courses/:id/analytics/player-retentionGet retention metrics
🧭GET/admin/courses/:id/analytics/forecastGet revenue forecast

Course Status Endpoints

Status: ✅ Available via CourseStatusController at /admin/courses/:courseId/status.

Core Status Operations:

StatusMethodPathDescription
GET/admin/courses/:id/status/:teeSheetIdGet current course status (playability, override, cart suspension)
GET/admin/courses/:id/status/historyGet condition change history for date range
POST/admin/courses/:id/status/overrideApply manual status override with expiry
POST/admin/courses/:id/status/clear-overrideClear manual override, optionally re-evaluate

Backend Service: CourseStatusService (libs/tee-time-services/src/lib/course-status/)

  • State machine: OPEN → CONDITIONAL → CLOSED with hysteresis thresholds
  • Automatic transitions via WeatherAlertScheduler integration
  • Manual overrides with configurable expiry (default: 4 hours)
  • Full audit trail via CourseConditionLog model

Course Conditions Endpoints (Future)

Status: 🧭 planned for Phase 13 completion.

StatusMethodPathDescription
🧭GET/admin/courses/:id/conditions/holesGet per-hole status (open/closed/GUR/temp green)
🧭PATCH/admin/courses/:id/conditions/holes/:holeUpdate hole status
🧭GET/admin/courses/:id/pin-positions/:dateGet pin positions
🧭PUT/admin/courses/:id/pin-positions/:dateSet pin positions

Data Models

TeeSheet

interface TeeSheet {
date: string; // ISO 8601 date
courseId: string;
courseName: string;
clubCode: string;
isNineHoles: boolean;
isPublicHoliday: boolean;
intervalMinutes: number; // 6, 7, 8, 9, 10, 12
crossoverBreak: number; // minutes between front/back 9
offlineReason?: string;
teeTimes: TeeTime[];
}

TeeTime

interface TeeTime {
uuid: string;
dateTime: Date;
timeString: string; // "06:00", "06:08"
tee: number; // 1 = front, 10 = back
slots: TeeTimeSlot[];
openSlots: number;
appliedRate: string; // Rate name
rateId?: number;
price: number; // Cents
isSpecial: boolean;
specialDescription?: string;
isBlocked: boolean;
blockDescription?: string;
blockType?: 'EVENT' | 'MAINTENANCE' | 'COMPETITION' | 'MANUAL';
}

TeeTimeSlot

interface TeeTimeSlot {
slot: number; // 1-4
status: 'AVAILABLE' | 'HELD' | 'BOOKED' | 'BLOCKED';
booking?: {
bookingId: string;
playerName: string;
playerId?: string;
memberNumber?: string;
classification: string; // 'MEMBER' | 'GUEST' | 'VISITOR'
paidAmount: number;
bookedAt: Date;
bookedBy: string; // User who made booking
};
}

Block

interface Block {
id: string;
courseId: string;
name: string;
type: 'EVENT' | 'MAINTENANCE' | 'COMPETITION' | 'MANUAL';
description?: string;
startDate: string; // ISO 8601
endDate: string;
startTime: string; // "06:00"
endTime: string; // "09:00"
tees: number[]; // [1], [10], [1, 10]
includeCrossover: boolean;
recurrence?: {
type: 'NONE' | 'WEEKLY' | 'MONTHLY';
daysOfWeek?: string[]; // ['MON', 'WED']
dayOfMonth?: number; // 1-31
endDate?: string;
};
createdAt: Date;
createdBy: string;
}

MemberRule

interface MemberRule {
id: string;
courseId: string;
name: string;
daysAhead: number; // Booking window
classifications: string[]; // ['MEMBER', 'SOCIAL']
associations?: string[]; // ['SAGA', 'OPEN_FAIRWAYS']
statuses?: string[]; // ['ACTIVE', 'SENIOR']
daysOfWeek: string[]; // ['MON', 'TUE', ...]
tees: number[]; // [1, 10]
timeWindow?: {
start: string; // "06:00"
end: string; // "18:00"
};
enabled: boolean;
priority: number;
}

RateRule

interface RateRule {
id: string;
courseId: string;
name: string;
baseRateCents: number;
currency: string; // 'ZAR', 'GBP'
classifications: string[]; // Who this rate applies to
daysOfWeek: string[];
tees: number[];
timeWindow?: {
start: string;
end: string;
};
validFrom?: string;
validTo?: string;
enabled: boolean;
priority: number;
}

Weather

interface WeatherForecast {
clubId: string;
location: {
latitude: number;
longitude: number;
timezone: string;
};
current: WeatherCondition;
hourly: HourlyForecast[];
daily: DailyForecast[];
alerts: WeatherAlert[];
lastUpdated: Date;
}

interface WeatherCondition {
temperature: number; // Celsius
feelsLike: number;
humidity: number; // Percentage
windSpeed: number; // km/h
windDirection: string; // 'N', 'NE', 'E', etc.
windGust?: number;
precipitation: number; // mm
precipitationProbability: number; // Percentage
uvIndex: number;
visibility: number; // km
cloudCover: number; // Percentage
condition: 'CLEAR' | 'PARTLY_CLOUDY' | 'CLOUDY' | 'RAIN' | 'THUNDERSTORM' | 'FOG' | 'SNOW';
description: string; // "Partly cloudy"
icon: string; // Icon code
}

interface HourlyForecast {
time: Date;
temperature: number;
feelsLike: number;
precipitationProbability: number;
precipitation: number;
windSpeed: number;
windGust?: number;
condition: string;
icon: string;
playingAdvice: PlayingAdvice;
}

interface DailyForecast {
date: string;
sunrise: string;
sunset: string;
tempHigh: number;
tempLow: number;
precipitationProbability: number;
precipitationTotal: number;
windSpeed: number;
condition: string;
icon: string;
uvIndex: number;
playingAdvice: PlayingAdvice;
}

interface PlayingAdvice {
rating: 'EXCELLENT' | 'GOOD' | 'FAIR' | 'POOR' | 'UNPLAYABLE';
score: number; // 0-100
factors: PlayingFactor[];
recommendation: string; // "Great conditions for golf"
warnings: string[]; // ["High UV - wear sunscreen", "Afternoon thunderstorms possible"]
}

interface PlayingFactor {
factor: 'TEMPERATURE' | 'WIND' | 'RAIN' | 'LIGHTNING' | 'UV' | 'VISIBILITY';
impact: 'POSITIVE' | 'NEUTRAL' | 'NEGATIVE' | 'SEVERE';
description: string;
}

interface WeatherAlert {
id: string;
type: 'LIGHTNING' | 'SEVERE_WEATHER' | 'HEAT' | 'WIND' | 'FLOOD' | 'FOG';
severity: 'WATCH' | 'WARNING' | 'ADVISORY';
title: string;
description: string;
startTime: Date;
endTime: Date;
affectedTimes?: string[]; // Tee times that may be affected
}

interface WeatherSettings {
clubId: string;
thresholds: {
windSpeedWarning: number; // km/h - show warning
windSpeedSevere: number; // km/h - show severe
temperatureHotWarning: number; // °C
temperatureColdWarning: number; // °C
uvIndexWarning: number;
rainProbabilityWarning: number; // %
};
autoBlock: {
enabled: boolean;
lightningRadius: number; // km - auto-block if lightning within
blockDurationMinutes: number;
};
notifications: {
alertAdmins: boolean;
alertBookedPlayers: boolean;
};
}

Cart & Item Response DTOs (Implemented)

These DTOs are returned by CourseInventoryController endpoints:

// GET/POST /v1/courses/:courseId/carts
interface CartInventoryDto {
id: string;
name: string;
type: 'electric' | 'pull' | 'push'; // Lowercase for frontend
totalUnits: number;
availableUnits: number;
reservedUnits: number;
pricePerRound: number; // Cents (converted from DB decimals)
status: 'active' | 'inactive';
}

// GET/POST /v1/courses/:courseId/items
interface ItemInventoryDto {
id: string;
name: string;
category: 'rental' | 'sale' | 'snack'; // Lowercase for frontend
stockLevel: number;
lowStockThreshold: number;
price: number; // Cents
status: 'in_stock' | 'low_stock' | 'out_of_stock'; // Auto-computed
}

// GET /v1/courses/:courseId/cart-reservations?date=YYYY-MM-DD
interface CartReservationDto {
id: string;
cartType: string; // Cart name
bookingId: string;
playerName: string;
teeTime: string; // "HH:mm" format
status: 'pending' | 'confirmed' | 'returned';
}

GolfCart (Facilities model - for reference)

interface GolfCart {
id: string;
courseId: string;
cartNumber: string; // "C01", "C02"
type: 'ELECTRIC' | 'GAS' | 'PULL' | 'PUSH';
capacity: number; // Usually 2
status: 'AVAILABLE' | 'RESERVED' | 'IN_USE' | 'MAINTENANCE' | 'OUT_OF_SERVICE';
features: string[]; // ['GPS', 'USB_CHARGER', 'COOLER']
notes?: string;
lastMaintenance?: Date;
nextMaintenance?: Date;
createdAt: Date;
}

interface CartInventory {
courseId: string;
date: string;
totalCarts: number;
available: number;
reserved: number;
inUse: number;
maintenance: number;
reservations: CartReservation[];
}

interface CartReservation {
id: string;
cartId: string;
cartNumber: string;
teeTimeUuid: string;
teeTime: string; // "06:00"
bookingId: string;
playerName: string;
status: 'PENDING' | 'CONFIRMED' | 'CHECKED_OUT' | 'RETURNED' | 'CANCELLED';
checkOutTime?: Date;
returnTime?: Date;
notes?: string;
}

interface CartPricing {
courseId: string;
prices: CartPrice[];
}

interface CartPrice {
type: 'ELECTRIC' | 'GAS' | 'PULL' | 'PUSH';
holes: 9 | 18;
memberPrice: number; // Cents
guestPrice: number;
visitorPrice: number;
includedWithRate?: string[]; // Rate IDs where cart is included
}

AdditionalItem

interface AdditionalItem {
id: string;
courseId: string;
categoryId: string;
name: string; // "Range Balls (50)", "Club Rental", "Caddie"
description?: string;
type: 'EQUIPMENT' | 'SERVICE' | 'FOOD_BEVERAGE' | 'MERCHANDISE';
pricing: {
memberPrice: number; // Cents
guestPrice: number;
visitorPrice: number;
};
availability: {
enabled: boolean;
requiresAdvanceBooking: boolean;
advanceBookingHours?: number; // Must book X hours ahead
limitPerBooking?: number; // Max quantity per booking
dailyLimit?: number; // Total available per day
};
schedule: {
daysOfWeek: string[]; // When available
timeWindow?: {
start: string;
end: string;
};
};
displayOrder: number;
imageUrl?: string;
createdAt: Date;
updatedAt: Date;
}

interface ItemCategory {
id: string;
courseId: string;
name: string; // "Equipment", "Services", "F&B"
description?: string;
displayOrder: number;
icon?: string;
}

interface BookingItem {
id: string;
bookingId: string;
itemId: string;
itemName: string;
quantity: number;
unitPrice: number;
totalPrice: number;
status: 'PENDING' | 'CONFIRMED' | 'DELIVERED' | 'CANCELLED';
notes?: string;
}

Real-time & Check-in

interface WebSocketMessage {
type: 'BOOKING_CREATED' | 'BOOKING_CANCELLED' | 'CHECK_IN' | 'TEE_OFF' |
'SLOT_BLOCKED' | 'SLOT_UNBLOCKED' | 'WEATHER_ALERT' | 'PACE_ALERT';
courseId: string;
teeTimeUuid?: string;
payload: unknown;
timestamp: Date;
}

interface CheckIn {
id: string;
bookingId: string;
playerId: string;
playerName: string;
teeTimeUuid: string;
teeTime: string;
checkedInAt: Date;
checkedInBy: string; // Staff who checked in
method: 'MANUAL' | 'QR_SCAN' | 'KIOSK' | 'AUTO';
bagDropNumber?: string;
cartAssigned?: string;
notes?: string;
}

interface StarterCall {
teeTimeUuid: string;
calledAt: Date;
calledBy: string;
playersNotified: number;
method: 'PA_SYSTEM' | 'SMS' | 'APP_PUSH';
}

Pace of Play

interface PaceOfPlay {
courseId: string;
date: string;
expectedRoundTime: number; // Minutes (e.g., 240 for 4 hours)
currentAverage: number; // Current average round time
groupsOnCourse: number;
alerts: PaceAlert[];
holes: HolePace[];
}

interface HolePace {
hole: number;
expectedTime: number; // Minutes
currentAverage: number;
status: 'CLEAR' | 'SLOW' | 'BACKED_UP';
groupsWaiting: number;
}

interface PaceAlert {
id: string;
groupId: string; // teeTimeUuid
teeTime: string;
currentHole: number;
minutesBehind: number;
severity: 'WARNING' | 'CRITICAL';
alertedAt?: Date;
players: string[];
}

interface RoundProgress {
bookingId: string;
teeTimeUuid: string;
teeOffTime?: Date;
currentHole: number;
holeCompletedTimes: { hole: number; completedAt: Date }[];
roundCompleteTime?: Date;
totalTime?: number; // Minutes
paceStatus: 'ON_PACE' | 'AHEAD' | 'BEHIND';
minutesFromPace: number;
}

Waitlist

Implemented DTOs (from WaitlistAdminController):

// GET /admin/waitlist/courses/:courseId/settings
interface CourseWaitlistSettingsDto {
id: string;
courseId: string;
enabled: boolean; // Enable/disable waitlist for course
maxQueueSize: number; // Max entries per slot (default: 10)
offerExpiryMinutes: number; // Time to accept offer (default: 30)
timeWindowMinutes: number; // Flexible time window (default: 60)
maxNotifications: number; // Max notifications per entry (default: 3)
autoExpireStale: boolean; // Auto-expire stale entries (default: true)
staleExpiryMinutes: number; // Stale threshold (default: 30)
autoCascade: boolean; // Auto-cascade on decline (default: true)
}

// GET /admin/waitlist/courses/:courseId/analytics
interface WaitlistAnalyticsDto {
courseId: string;
startDate: string;
endDate: string;
entries: {
total: number;
active: number;
notified: number;
booked: number;
cancelled: number;
};
offers: {
total: number;
pending: number;
accepted: number;
declined: number;
expired: number;
avgResponseTimeMs: number | null;
};
conversionRate: number; // Percentage (booked / total)
acceptanceRate: number; // Percentage (accepted / total offers)
}

// WaitlistOffer (persisted to database)
interface WaitlistOffer {
id: string;
tenantId: string;
waitlistEntryId: string;
slotId: string;
memberId: string;
courseId: string;
clubId: string;
status: 'PENDING' | 'ACCEPTED' | 'DECLINED' | 'EXPIRED';
offeredAt: Date;
expiresAt: Date;
respondedAt?: Date;
createdAt: Date;
updatedAt: Date;
}

Reference Models (for future enhancements):

interface WaitlistEntry {
id: string;
courseId: string;
date: string;
playerId: string;
playerName: string;
playerPhone: string;
playerEmail: string;
preferredTimes: {
start: string; // "06:00"
end: string; // "10:00"
};
preferredTee?: number; // 1 or 10
playersNeeded: number; // 1-4
flexibleDate: boolean; // Can accept different date
alternativeDates?: string[];
addedAt: Date;
status: 'WAITING' | 'OFFERED' | 'ACCEPTED' | 'EXPIRED' | 'CANCELLED';
offerExpiry?: Date;
offeredSlot?: {
teeTimeUuid: string;
time: string;
tee: number;
};
notes?: string;
}

interface WaitlistSettings {
courseId: string;
enabled: boolean;
maxPerDay: number; // Max waitlist entries per day
offerExpiryMinutes: number; // How long to accept offer
autoOfferEnabled: boolean; // Auto-offer when slot opens
notificationMethods: ('SMS' | 'EMAIL' | 'PUSH')[];
priorityRules: {
memberFirst: boolean;
earliestRequestFirst: boolean;
};
}

Player Intelligence

interface PlayerProfile {
id: string;
firstName: string;
lastName: string;
email: string;
phone: string;
memberNumber?: string;
classification: string;
handicapIndex?: number;
homeClub?: string;

// Stats
stats: {
totalBookings: number;
bookingsThisYear: number;
totalSpend: number;
spendThisYear: number;
averageGroupSize: number;
noShowCount: number;
noShowRate: number;
cancellationRate: number;
preferredTeeTime?: string; // Most booked time
preferredDay?: string; // Most booked day
lastVisit?: Date;
memberSince?: Date;
};

// Flags
flags: PlayerFlag[];

// Preferences
preferences: PlayerPreferences;

// Buddies
frequentPartners: {
playerId: string;
name: string;
roundsTogether: number;
}[];
}

interface PlayerFlag {
id: string;
type: 'VIP' | 'WARNING' | 'NO_SHOW_RISK' | 'SLOW_PLAY' | 'SPONSOR' | 'STAFF' | 'CUSTOM';
label: string;
description?: string;
color: string;
icon: string;
addedAt: Date;
addedBy: string;
expiresAt?: Date;
}

interface PlayerPreferences {
preferredTee: number;
preferredTime: string;
cartPreference: 'ALWAYS' | 'SOMETIMES' | 'NEVER';
buddyList: string[]; // Player IDs
communicationPreferences: {
bookingConfirmation: boolean;
dayBeforeReminder: boolean;
weatherAlerts: boolean;
promotions: boolean;
channels: ('EMAIL' | 'SMS' | 'PUSH')[];
};
}

interface NoShowRecord {
id: string;
playerId: string;
bookingId: string;
date: string;
teeTime: string;
reason?: string;
penalty?: {
type: 'WARNING' | 'FEE' | 'SUSPENSION';
amount?: number;
duration?: number; // Days of suspension
};
appealStatus?: 'NONE' | 'PENDING' | 'APPROVED' | 'DENIED';
createdAt: Date;
}

Communication

interface NotificationTemplate {
id: string;
courseId: string;
type: 'BOOKING_CONFIRMATION' | 'REMINDER_24H' | 'REMINDER_2H' | 'CANCELLATION' |
'WEATHER_ALERT' | 'WAITLIST_OFFER' | 'PACE_WARNING' | 'CUSTOM';
name: string;
channel: 'EMAIL' | 'SMS' | 'PUSH';
subject?: string; // For email
body: string; // Supports {{variables}}
enabled: boolean;
variables: string[]; // Available merge fields
}

interface NotificationSettings {
courseId: string;
confirmationEnabled: boolean;
reminder24hEnabled: boolean;
reminder2hEnabled: boolean;
weatherAlertsEnabled: boolean;
defaultChannels: ('EMAIL' | 'SMS' | 'PUSH')[];
smsProvider?: string;
emailFromAddress?: string;
quietHours?: {
start: string; // "21:00"
end: string; // "07:00"
};
}

interface NotificationLog {
id: string;
bookingId?: string;
playerId: string;
templateId?: string;
channel: 'EMAIL' | 'SMS' | 'PUSH';
recipient: string;
subject?: string;
body: string;
sentAt: Date;
status: 'PENDING' | 'SENT' | 'DELIVERED' | 'FAILED' | 'BOUNCED';
errorMessage?: string;
}

Tournament & Group Booking

interface Tournament {
id: string;
courseId: string;
name: string;
type: 'MEDAL' | 'STABLEFORD' | 'MATCHPLAY' | 'BETTERBALL' | 'SCRAMBLE' | 'CUSTOM';
format: {
holes: 9 | 18;
handicapAllowance: number; // Percentage (e.g., 75, 90, 100)
maxHandicap?: number;
teamSize?: number; // For team events
};
dates: {
start: string;
end: string;
};
startType: 'TEE_TIMES' | 'SHOTGUN';
shotgunTime?: string; // For shotgun start
maxPlayers: number;
registeredPlayers: number;
entryFee?: number;
prizes?: string;
status: 'DRAFT' | 'OPEN' | 'CLOSED' | 'IN_PROGRESS' | 'COMPLETE' | 'CANCELLED';
blockId?: string; // Associated tee sheet block
flights: TournamentFlight[];
createdAt: Date;
createdBy: string;
}

interface TournamentFlight {
id: string;
tournamentId: string;
name: string; // "A Flight", "B Flight"
handicapRange?: {
min: number;
max: number;
};
startingHole?: number; // For shotgun
players: TournamentPlayer[];
}

interface TournamentPlayer {
playerId: string;
playerName: string;
handicap: number;
flightId?: string;
groupNumber?: number;
startingHole?: number;
teeTimeUuid?: string;
status: 'REGISTERED' | 'CONFIRMED' | 'WITHDRAWN' | 'DNS' | 'DNF' | 'DQ';
}

interface GroupBooking {
id: string;
courseId: string;
name: string; // "ABC Corp Golf Day"
organizer: {
name: string;
email: string;
phone: string;
company?: string;
};
date: string;
startTime: string;
endTime: string;
playerCount: number;
teeTimes: string[]; // Booked tee time UUIDs
package?: {
name: string;
pricePerPlayer: number;
includes: string[]; // ['Green Fee', 'Cart', 'Lunch']
};
specialRequests?: string;
status: 'INQUIRY' | 'QUOTED' | 'CONFIRMED' | 'DEPOSIT_PAID' | 'PAID' | 'COMPLETE' | 'CANCELLED';
depositAmount?: number;
depositPaid?: boolean;
totalAmount: number;
blockId?: string; // Associated tee sheet block
createdAt: Date;
}

Analytics

interface DemandHeatmap {
courseId: string;
period: 'WEEK' | 'MONTH' | 'QUARTER';
data: {
dayOfWeek: number; // 0-6
hour: number; // 6-18
demand: number; // 0-100 (percentage)
avgUtilization: number;
avgPrice: number;
bookingCount: number;
}[];
}

interface RevenueAnalytics {
courseId: string;
period: {
start: string;
end: string;
};
summary: {
totalRevenue: number;
greenFees: number;
cartRevenue: number;
itemsRevenue: number;
avgRevenuePerRound: number;
avgRevenuePerPlayer: number;
compareLastPeriod: number; // Percentage change
};
daily: {
date: string;
revenue: number;
bookings: number;
utilization: number;
}[];
byChannel: {
channel: string;
revenue: number;
percentage: number;
}[];
byPlayerType: {
type: string;
revenue: number;
bookings: number;
}[];
}

interface UtilizationTrends {
courseId: string;
period: {
start: string;
end: string;
};
overall: number;
byDayOfWeek: { day: string; utilization: number }[];
byTimeSlot: { time: string; utilization: number }[];
peakTimes: { time: string; day: string; utilization: number }[];
lowTimes: { time: string; day: string; utilization: number }[];
}

interface RevenueForecast {
courseId: string;
forecastPeriod: {
start: string;
end: string;
};
predictedRevenue: number;
confidence: number; // 0-100
factors: {
factor: string;
impact: 'POSITIVE' | 'NEGATIVE' | 'NEUTRAL';
description: string;
}[];
daily: {
date: string;
predicted: number;
actual?: number;
weather?: string;
}[];
}

Course Status (Implemented)

These DTOs are returned by CourseStatusController endpoints:

// GET /admin/courses/:id/status/:teeSheetId
interface CourseStatusResponseDto {
status: 'OPEN' | 'CONDITIONAL' | 'CLOSED';
playabilityScore: number | null; // 0-100, weather-derived
manualOverride: boolean; // Whether override is active
overrideReason: string | null; // Human-readable reason
overrideExpiry: string | null; // ISO timestamp when override expires
cartsSuspended: boolean; // Whether carts are suspended
}

// POST /admin/courses/:id/status/override
interface ManualOverrideDto {
teeSheetId: string; // UUID
date: string; // YYYY-MM-DD
newStatus: 'OPEN' | 'CONDITIONAL' | 'CLOSED';
reason: string; // Min 5 chars, displayed to players
expiryMinutes?: number; // Default: 240 (4 hours), min: 15
}

// POST /admin/courses/:id/status/clear-override
interface ClearOverrideDto {
teeSheetId: string;
date: string;
reEvaluate?: boolean; // Default: true, re-evaluate based on weather
}

// Response for POST /override
interface ApplyOverrideResponseDto {
success: boolean;
logId?: string; // Audit log entry ID
cartsSuspended?: boolean; // Whether carts were suspended
}

// Response for POST /clear-override
interface ClearOverrideResponseDto {
success: boolean;
newStatus?: string; // Status after re-evaluation
}

// GET /admin/courses/:id/status/history
interface ConditionLogResponseDto {
id: string;
date: string; // ISO timestamp
previousStatus: string | null;
newStatus: string;
playabilityScore: number | null;
source: 'AUTO_WEATHER' | 'MANUAL_OVERRIDE' | 'SCHEDULED' | 'SYSTEM';
reason: string | null;
alertTypes: string[]; // e.g., ['HIGH_WIND', 'RAIN']
createdBy: string | null; // User ID for manual changes
createdAt: string; // ISO timestamp
}

// Database model (CourseConditionLog)
interface CourseConditionLog {
id: string;
tenantId: string;
teeSheetId: string;
courseId: string;
date: Date;
previousStatus: CourseStatus | null;
newStatus: CourseStatus;
playabilityScore: number | null;
source: ConditionChangeSource;
reason: string | null;
weatherSnapshot: Record<string, unknown> | null; // Wind/precip/temp at change time
alertTypes: string[];
notificationsSent: number;
bookingsAffected: number;
createdBy: string | null;
createdAt: Date;
}

// Enums
enum CourseStatus {
OPEN = 'OPEN',
CONDITIONAL = 'CONDITIONAL',
CLOSED = 'CLOSED',
}

enum ConditionChangeSource {
AUTO_WEATHER = 'AUTO_WEATHER',
MANUAL_OVERRIDE = 'MANUAL_OVERRIDE',
SYSTEM = 'SYSTEM',
}

Course Conditions (Future)

interface CourseConditions {
courseId: string;
updatedAt: Date;
updatedBy: string;
overall: 'EXCELLENT' | 'GOOD' | 'FAIR' | 'POOR' | 'CLOSED';
details: {
greens: 'EXCELLENT' | 'GOOD' | 'FAIR' | 'POOR';
greensSpeed: string; // "Stimp 10.5"
fairways: 'EXCELLENT' | 'GOOD' | 'FAIR' | 'POOR';
bunkers: 'EXCELLENT' | 'GOOD' | 'FAIR' | 'POOR';
rough: 'LIGHT' | 'MEDIUM' | 'HEAVY';
};
alerts: string[]; // ["GUR on hole 7", "Temp green on 15"]
cartPolicy: 'ALLOWED' | 'PATH_ONLY' | 'NO_CARTS';
walkingAllowed: boolean;
}

interface HoleStatus {
hole: number;
status: 'OPEN' | 'CLOSED' | 'TEMP_GREEN' | 'GUR';
notes?: string;
pinPosition?: {
position: 'FRONT' | 'MIDDLE' | 'BACK' | 'CUSTOM';
customDescription?: string;
difficulty: 'EASY' | 'MEDIUM' | 'HARD';
};
}

interface PinSheet {
courseId: string;
date: string;
holes: {
hole: number;
position: string;
distance: string; // "5 paces from front, 3 left"
difficulty: 'EASY' | 'MEDIUM' | 'HARD';
}[];
createdAt: Date;
createdBy: string;
}

Data Formats

FieldFormatExample
TimesHH:mm (24h)06:00, 14:30
DatesISO 86012025-12-25
Days of week3-letter codesMON, TUE, WED
Currency3-letter ISOZAR, GBP, USD
AmountsCents (integer)55000 = R550.00

Revision History

DateAuthorChanges
2025-12-09Rudi HaarhoffInitial API specification
2025-12-10Rudi HaarhoffAdded Facilities hybrid fields to Available list
2025-12-10Rudi HaarhoffCart/item endpoints marked Available with correct paths; added CartInventoryDto, ItemInventoryDto, CartReservationDto
2025-12-10Rudi HaarhoffBooking→Facilities hooks deferred; added status tags to real-time/check-in/pace endpoints; documented SSE infrastructure
2025-12-11Rudi HaarhoffWaitlist endpoints marked Available; added WaitlistController + WaitlistAdminController paths; added CourseWaitlistSettingsDto, WaitlistAnalyticsDto, WaitlistOffer DTOs
2025-12-12Codex AgentUpdated statuses to match shipped endpoints (admin tee sheet, blocks/rules CRUD, check-in/pace, WebSocket gateway, weather paths); clarified backlog items
2025-12-11ClaudeAdded Course Status endpoints (CourseStatusController) with DTOs: CourseStatusResponseDto, ManualOverrideDto, ClearOverrideDto, ConditionLogResponseDto, CourseConditionLog; separated from planned Course Conditions (hole status, pins)