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-adminaudience; 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-Counton list endpoints. - Time formats: Dates ISO (
2025-12-25); TimesHH:mmor ISO timestamps where noted. - Idempotency:
externalReffor booking-adjacent writes; see provider-specific notes.
Status Legend & Conventions
| Tag | Meaning |
|---|---|
| ✅ Available | Implemented/shipping |
| 🧭 Required | Backlog item |
- Headers:
Authorization: Bearer <token>(audienceteetime-admin),Content-Type: application/json. - Errors: JSON
{ error: string; message: string; statusCode: number; details?: Record<string, unknown> }. - Pagination:
?page=1&limit=50; totals inX-Total-Count. - Idempotency: supply
externalRefon 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)
| Status | Method | Path | Description |
|---|---|---|---|
| ✅ | GET | /admin/clubs | List clubs for selector |
| ✅ | GET | /admin/clubs/:id/courses | List courses for a club |
| ✅ | GET | /admin/courses/:id/tee-sheet/:date | Daily tee sheet with slots, rates, and stats |
| ✅ | GET | /admin/courses/:id/tee-sheet/settings | Get tee sheet configuration |
| ✅ | PUT | /admin/courses/:id/tee-sheet/settings | Update tee sheet configuration |
| ✅ | PATCH | /admin/tee-sheets/:id/offline | Set online/offline status |
| ✅ | GET | /admin/courses/:id/blocks | List blocks for a course |
| ✅ | POST | /admin/courses/:id/blocks | Create a block (single or recurring) |
| ✅ | PUT | /admin/courses/:id/blocks/:id | Update a block |
| ✅ | DELETE | /admin/courses/:id/blocks/:id | Delete a block |
| ✅ | GET | /admin/courses/:id/rules/member | List member booking rules |
| ✅ | POST | /admin/courses/:id/rules/member | Create member rule |
| ✅ | PUT | /admin/courses/:id/rules/member/:id | Update member rule |
| ✅ | DELETE | /admin/courses/:id/rules/member/:id | Delete member rule |
| ✅ | GET | /admin/courses/:id/rules/rate | List rate rules |
| ✅ | POST | /admin/courses/:id/rules/rate | Create rate rule |
| ✅ | PUT | /admin/courses/:id/rules/rate/:id | Update rate rule |
| ✅ | DELETE | /admin/courses/:id/rules/rate/:id | Delete rate rule |
| ✅ | GET | /admin/courses/:id/starter-sheet | Generate starter sheet PDF |
| ✅ | Fields | useFacilitiesCarts + facilitiesClubId on courses enable Facilities-powered carts (tee-time carts become read-only; items stay tee-time native) | |
| ✅ | Fields | weatherThresholds (wind/precip/playability + auto-block) persisted with tee-sheet settings |
Required / backlog
| Status | Method | Path | Description |
|---|---|---|---|
| 🧭 | GET | /admin/courses/:id/tee-times | Dedicated tee-time search with filters (current daily view endpoint is used instead) |
| 🧭 | GET | /admin/courses/:id/tee-times/:uuid | Get single tee time details |
| 🧭 | POST | /admin/courses/:id/tee-times/:uuid/block | Block a tee time |
| 🧭 | DELETE | /admin/courses/:id/tee-times/:uuid/block | Unblock a tee time |
| 🧭 | GET | /admin/courses/:id/tee-sheet/:date/stats | Get daily statistics |
Weather Endpoints
Status: ✅ available unless noted.
| Status | Method | Path | Description |
|---|---|---|---|
| ✅ | GET | /v1/courses/:id/weather | Course-scoped snapshot with playability score, hourly projection, auto-block on severe |
| ✅ | GET | /weather/forecast | 7-day weather forecast (lat/lon or free-text location) |
| ✅ | GET | /weather/current | Current conditions (lat/lon or free-text location) |
| ✅ | GET | /weather/hourly | Hourly forecast for a date |
| ✅ | GET | /weather/courses/:id/forecast | Course forecast using stored coordinates |
| 🧭 | PUT | /admin/clubs/:id/weather/settings | Configure 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.
| Status | Method | Path | Description |
|---|---|---|---|
| ✅ | GET | /v1/courses/:courseId/carts | List cart inventory |
| ✅ | POST | /v1/courses/:courseId/carts | Add cart to inventory |
| ✅ | PUT | /v1/courses/:courseId/carts/:id | Update cart details |
| ✅ | DELETE | /v1/courses/:courseId/carts/:id | Remove cart from inventory |
| ✅ | GET | /v1/courses/:courseId/cart-reservations | List reservations by date (?date=YYYY-MM-DD) |
| 🧭 | GET | /v1/courses/:courseId/carts/availability/:date | Get cart availability summary for date |
| 🧭 | POST | /v1/courses/:courseId/carts/reserve | Reserve cart for booking |
| 🧭 | DELETE | /v1/courses/:courseId/cart-reservations/:id | Cancel cart reservation |
| 🧭 | POST/DELETE | /bookings/:id/cart-assignment | Planned: 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).
| Status | Method | Path | Description |
|---|---|---|---|
| ✅ | GET | /v1/courses/:courseId/items | List bookable additional items |
| ✅ | POST | /v1/courses/:courseId/items | Create additional item |
| ✅ | PUT | /v1/courses/:courseId/items/:id | Update item details/pricing (auto-updates status based on stock) |
| ✅ | DELETE | /v1/courses/:courseId/items/:id | Remove item |
| 🧭 | PATCH | /v1/courses/:courseId/items/:id/availability | Toggle item availability |
| 🧭 | GET | /v1/courses/:courseId/items/categories | List item categories |
| 🧭 | POST | /v1/courses/:courseId/items/categories | Create 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:
- Tee-time bookings use UUID; Facilities expects numeric
bookingId - Need
facilitiesAssignmentIdfield on tee-time Booking model - Need Facilities
findActiveAssignmentByBookingId+returnCartByBookingIdmethods - Cart selection not yet captured in booking flow
- Tee-time bookings use UUID; Facilities expects numeric
- 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.
| Status | Method | Path | Description |
|---|---|---|---|
| ✅ | GET | /clubs/tee-sheet/stream | SSE stream for multi-club tee sheet data |
| ✅ | WS | /ws/tee-sheet | WebSocket for live slot/booking/check-in/pace events (rooms per club/course/date/booking) |
Check-in Endpoints
Status: ✅ available (Phase 7 shipped).
| Status | Method | Path | Description |
|---|---|---|---|
| ✅ | POST | /admin/bookings/:id/check-in | Check in a player |
| ✅ | POST | /admin/bookings/:id/check-in/qr | Check in via QR code scan |
| ✅ | DELETE | /admin/bookings/:id/check-in | Undo check-in |
| ✅ | GET | /admin/courses/:id/check-ins/:date | Get all check-ins for date |
| ✅ | POST | /admin/courses/:id/starter-call | Announce tee time ready |
Pace of Play Endpoints
Status: ✅ available (Phase 7 shipped).
| Status | Method | Path | Description |
|---|---|---|---|
| ✅ | GET | /admin/courses/:id/pace/:date | Get pace of play for date |
| ✅ | POST | /admin/bookings/:id/tee-off | Record tee-off time |
| ✅ | POST | /admin/bookings/:id/hole-complete | Record hole completion |
| ✅ | POST | /admin/bookings/:id/round-complete | Record round completion |
| ✅ | GET | /admin/courses/:id/pace/alerts | Get slow play alerts |
| ✅ | POST | /admin/courses/:id/pace/alert/:groupId | Send pace warning to group |
Waitlist Endpoints
Status: ✅ Available via WaitlistController at /admin/courses/:courseId/waitlist and WaitlistAdminController at /admin/waitlist.
Core Waitlist Operations (WaitlistController):
| Status | Method | Path | Description |
|---|---|---|---|
| ✅ | GET | /admin/courses/:id/waitlist/:date | Get waitlist entries for date |
| ✅ | POST | /admin/courses/:id/waitlist | Join waitlist for a slot |
| ✅ | DELETE | /admin/courses/:id/waitlist/:tenantId/:slotId/:memberId | Leave waitlist |
| ✅ | GET | /admin/courses/:id/waitlist/position/:slotId/:memberId | Get queue position |
| ✅ | GET | /admin/courses/:id/waitlist/count/:slotId | Get waitlist count for slot |
| ✅ | GET | /admin/courses/:id/waitlist/member/:tenantId/:memberId | Get member's active waitlists |
| ✅ | POST | /admin/courses/:id/waitlist/:entryId/offer | Manually offer slot to entry |
| ✅ | POST | /admin/courses/:id/waitlist/:entryId/accept | Accept offered slot |
| ✅ | POST | /admin/courses/:id/waitlist/:entryId/booked | Mark entry as booked (internal) |
Admin Settings & Analytics (WaitlistAdminController):
| Status | Method | Path | Description |
|---|---|---|---|
| ✅ | GET | /admin/waitlist/courses/:courseId/settings | Get per-course waitlist settings |
| ✅ | PUT | /admin/waitlist/courses/:courseId/settings | Update per-course waitlist settings |
| ✅ | DELETE | /admin/waitlist/courses/:courseId/settings | Reset settings to defaults |
| ✅ | GET | /admin/waitlist/courses/:courseId/analytics | Get waitlist performance analytics |
| ✅ | POST | /admin/waitlist/courses/:courseId/expire-stale | Bulk expire stale entries |
| ✅ | POST | /admin/waitlist/courses/:courseId/cascade | Cascade offers on cancellation |
Database Models:
WaitlistEntry- Waitlist queue entries with FIFO orderingWaitlistOffer- Persistent offer tracking with status (PENDING, ACCEPTED, DECLINED, EXPIRED)CourseWaitlistSettings- Per-course configuration with defaults fallback
Player Intelligence Endpoints
Status: 🧭 planned unless noted.
| Status | Method | Path | Description |
|---|---|---|---|
| 🧭 | GET | /admin/players/:id/profile | Get full player profile |
| 🧭 | GET | /admin/players/:id/history | Get booking history |
| 🧭 | GET | /admin/players/:id/stats | Get player statistics |
| 🧭 | GET | /admin/players/:id/no-shows | Get no-show history |
| 🧭 | POST | /admin/players/:id/flags | Add player flag (VIP, Warning) |
| 🧭 | DELETE | /admin/players/:id/flags/:flagId | Remove player flag |
| 🧭 | GET | /admin/players/:id/preferences | Get player preferences |
| 🧭 | PUT | /admin/players/:id/preferences | Update preferences |
Communication Endpoints
Status: Partially available via TeeTimeMessagingService (library-level, not REST).
Event-Driven Messaging (✅ Available - internal services):
| Status | Service Method | Description |
|---|---|---|
| ✅ | 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):
| Status | Method | Path | Description |
|---|---|---|---|
| 🧭 | POST | /admin/bookings/:id/notify | Send notification to player |
| 🧭 | POST | /admin/tee-times/:uuid/notify-all | Notify entire fourball |
| 🧭 | GET | /admin/courses/:id/notifications/templates | List notification templates |
| 🧭 | PUT | /admin/courses/:id/notifications/templates/:id | Update template |
| 🧭 | GET | /admin/courses/:id/notifications/settings | Get notification settings |
| 🧭 | PUT | /admin/courses/:id/notifications/settings | Update settings |
| 🧭 | POST | /admin/courses/:id/notifications/bulk | Send 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.
| Status | Method | Path | Description |
|---|---|---|---|
| 🧭 | GET | /admin/courses/:id/tournaments | List tournaments |
| 🧭 | POST | /admin/courses/:id/tournaments | Create tournament |
| 🧭 | GET | /admin/courses/:id/tournaments/:tournamentId | Get tournament details |
| 🧭 | PUT | /admin/courses/:id/tournaments/:tournamentId | Update tournament |
| 🧭 | DELETE | /admin/courses/:id/tournaments/:tournamentId | Delete tournament |
| 🧭 | POST | /admin/courses/:id/tournaments/:tournamentId/draw | Generate draw |
| 🧭 | GET | /admin/courses/:id/tournaments/:tournamentId/flights | Get flights |
| 🧭 | POST | /admin/courses/:id/tournaments/:tournamentId/flights | Create/update flights |
| 🧭 | POST | /admin/courses/:id/group-booking | Create group/corporate booking |
| 🧭 | GET | /admin/courses/:id/group-bookings | List group bookings |
Analytics Endpoints
Status: 🧭 planned unless noted.
| Status | Method | Path | Description |
|---|---|---|---|
| 🧭 | GET | /admin/courses/:id/analytics/demand | Get demand heatmap data |
| 🧭 | GET | /admin/courses/:id/analytics/revenue | Get revenue analytics |
| 🧭 | GET | /admin/courses/:id/analytics/utilization | Get utilization trends |
| 🧭 | GET | /admin/courses/:id/analytics/cancellations | Get cancellation patterns |
| 🧭 | GET | /admin/courses/:id/analytics/player-retention | Get retention metrics |
| 🧭 | GET | /admin/courses/:id/analytics/forecast | Get revenue forecast |
Course Status Endpoints
Status: ✅ Available via CourseStatusController at /admin/courses/:courseId/status.
Core Status Operations:
| Status | Method | Path | Description |
|---|---|---|---|
| ✅ | GET | /admin/courses/:id/status/:teeSheetId | Get current course status (playability, override, cart suspension) |
| ✅ | GET | /admin/courses/:id/status/history | Get condition change history for date range |
| ✅ | POST | /admin/courses/:id/status/override | Apply manual status override with expiry |
| ✅ | POST | /admin/courses/:id/status/clear-override | Clear 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
WeatherAlertSchedulerintegration - Manual overrides with configurable expiry (default: 4 hours)
- Full audit trail via
CourseConditionLogmodel
Course Conditions Endpoints (Future)
Status: 🧭 planned for Phase 13 completion.
| Status | Method | Path | Description |
|---|---|---|---|
| 🧭 | GET | /admin/courses/:id/conditions/holes | Get per-hole status (open/closed/GUR/temp green) |
| 🧭 | PATCH | /admin/courses/:id/conditions/holes/:hole | Update hole status |
| 🧭 | GET | /admin/courses/:id/pin-positions/:date | Get pin positions |
| 🧭 | PUT | /admin/courses/:id/pin-positions/:date | Set 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
| Field | Format | Example |
|---|---|---|
| Times | HH:mm (24h) | 06:00, 14:30 |
| Dates | ISO 8601 | 2025-12-25 |
| Days of week | 3-letter codes | MON, TUE, WED |
| Currency | 3-letter ISO | ZAR, GBP, USD |
| Amounts | Cents (integer) | 55000 = R550.00 |
Revision History
| Date | Author | Changes |
|---|---|---|
| 2025-12-09 | Rudi Haarhoff | Initial API specification |
| 2025-12-10 | Rudi Haarhoff | Added Facilities hybrid fields to Available list |
| 2025-12-10 | Rudi Haarhoff | Cart/item endpoints marked Available with correct paths; added CartInventoryDto, ItemInventoryDto, CartReservationDto |
| 2025-12-10 | Rudi Haarhoff | Booking→Facilities hooks deferred; added status tags to real-time/check-in/pace endpoints; documented SSE infrastructure |
| 2025-12-11 | Rudi Haarhoff | Waitlist endpoints marked Available; added WaitlistController + WaitlistAdminController paths; added CourseWaitlistSettingsDto, WaitlistAnalyticsDto, WaitlistOffer DTOs |
| 2025-12-12 | Codex Agent | Updated statuses to match shipped endpoints (admin tee sheet, blocks/rules CRUD, check-in/pace, WebSocket gateway, weather paths); clarified backlog items |
| 2025-12-11 | Claude | Added Course Status endpoints (CourseStatusController) with DTOs: CourseStatusResponseDto, ManualOverrideDto, ClearOverrideDto, ConditionLogResponseDto, CourseConditionLog; separated from planned Course Conditions (hole status, pins) |