Course Status & Conditions
The course status system manages operational state (open/closed) based on weather conditions, with manual override capability and full audit trail.
State Machine
┌─────────────────┐
│ │
┌───────────▶│ OPEN │◀───────────┐
│ │ (playable) │ │
│ └────────┬────────┘ │
│ │ │
│ playability < warning │
│ ▼ │
│ ┌─────────────────┐ │
│ │ │ │
│ │ CONDITIONAL │ playability > recovery
│ │ (limited play) │ │
│ └────────┬────────┘ │
│ │ │
│ playability < severe │
│ ▼ │
│ ┌─────────────────┐ │
└────────────│ │────────────┘
│ CLOSED │
│ (unplayable) │
└─────────────────┘
Source: libs/tee-time-services/src/lib/course-status/course-status.service.ts
Status Levels
| Status | Description | Bookings | Cart Policy |
|---|---|---|---|
OPEN | Normal operations | Allowed | Normal |
CONDITIONAL | Limited play, weather advisory | Allowed with warning | May be restricted |
CLOSED | Course unplayable | Blocked | Suspended |
Playability Score
Weather conditions are converted to a 0-100 playability score:
| Score | Level | Status Trigger |
|---|---|---|
| 80-100 | Excellent | OPEN |
| 60-79 | Good | OPEN |
| 40-59 | Fair | CONDITIONAL (if < warning threshold) |
| 20-39 | Poor | CONDITIONAL |
| 0-19 | Unplayable | CLOSED (if < severe threshold) |
Calculation Factors
playabilityScore = 100
- windPenalty(windSpeed) // 0-30 points
- rainPenalty(precipitation) // 0-40 points
- tempPenalty(temperature) // 0-20 points
- lightningPenalty(alerts) // 0-50 points (immediate)
Thresholds
Per-course configurable thresholds:
interface CourseWeatherThresholds {
windWarningKmh: number; // Default: 40
windSevereKmh: number; // Default: 60
precipitationWarningMm: number; // Default: 5
playabilityWarningThreshold: number; // Default: 50
autoBlockOnSevere: boolean; // Default: false
}
Hysteresis
To prevent status flapping, recovery thresholds are higher than trigger thresholds:
// Trigger: playability drops below 50 → CONDITIONAL
// Recovery: playability must rise above 60 → OPEN
const HYSTERESIS_MARGIN = 10;
if (currentStatus === 'OPEN' && playability < warningThreshold) {
return 'CONDITIONAL';
}
if (currentStatus === 'CONDITIONAL' && playability > warningThreshold + HYSTERESIS_MARGIN) {
return 'OPEN';
}
Automatic Transitions
The WeatherAlertScheduler monitors conditions and triggers automatic transitions:
Schedule: Every 15 minutes (configurable)
// In WeatherAlertScheduler
async evaluateCourseConditions(courseId: string): Promise<void> {
const weather = await this.weatherService.getCurrent(courseId);
const playability = this.calculatePlayability(weather);
const thresholds = await this.getThresholds(courseId);
const newStatus = this.courseStatusService.evaluateStatus(
currentStatus,
playability,
thresholds
);
if (newStatus !== currentStatus) {
await this.courseStatusService.transitionStatus(
courseId,
newStatus,
'AUTO_WEATHER',
{ playability, weather }
);
}
}
Manual Overrides
Admins can manually override the course status:
// POST /admin/courses/:courseId/status
{
"status": "CLOSED",
"reason": "Course maintenance - aerating greens",
"source": "MANUAL"
}
Override Behavior
- Manual overrides take precedence over automatic transitions
- Override persists until explicitly cleared or end-of-day
- Logged in audit trail with actor identification
Side Effects
On CLOSED Transition
- Cart Suspension: All cart inventory marked INACTIVE
- Reservation Cleanup: Pending cart reservations marked RETURNED
- Player Notification: Affected bookings notified (if enabled)
- WebSocket Broadcast:
course.status.changedevent emitted - Outbox Event: Durable event for async processing
// In CourseStatusChangedListener
@OnEvent('course.status.changed')
async handleStatusChanged(event: CourseStatusChangedEvent) {
if (event.newStatus === 'CLOSED') {
await this.suspendCarts(event.courseId);
await this.notifyAffectedBookings(event);
}
await this.publishWebSocket(event);
await this.publishOutbox(event);
}
On OPEN Transition (from CLOSED)
- Cart Resumption: Cart inventory restored to ACTIVE
- WebSocket Broadcast: Status change event
- No automatic rebooking: Players must rebook manually
Audit Trail
All status changes are logged:
interface CourseConditionLog {
id: string;
courseId: string;
teeSheetId: string;
previousStatus: CourseStatus;
newStatus: CourseStatus;
source: 'AUTO_WEATHER' | 'MANUAL' | 'SCHEDULED';
reason?: string;
playabilityScore?: number;
weatherData?: JsonValue;
changedBy?: string; // User ID for manual changes
changedAt: Date;
}
Querying History
// GET /admin/courses/:courseId/status/history?date=2025-12-15
[
{
"previousStatus": "OPEN",
"newStatus": "CONDITIONAL",
"source": "AUTO_WEATHER",
"playabilityScore": 45,
"changedAt": "2025-12-15T08:30:00Z"
},
{
"previousStatus": "CONDITIONAL",
"newStatus": "CLOSED",
"source": "MANUAL",
"reason": "Lightning in area",
"changedBy": "admin@club.com",
"changedAt": "2025-12-15T09:15:00Z"
}
]
API Endpoints
| Method | Endpoint | Description |
|---|---|---|
GET | /admin/courses/:courseId/status | Get current status |
POST | /admin/courses/:courseId/status | Set status (manual) |
GET | /admin/courses/:courseId/status/history | Get audit log |
GET | /v1/courses/:courseId/conditions | Public status (player-facing) |
WebSocket Events
| Event | Payload |
|---|---|
course.status.changed | { previousStatus, newStatus, source, playabilityScore, reason, cartsSuspended } |
Admin UI
CourseConditionsPanel
Displays current status and allows manual control:
- Status indicator (color-coded)
- Playability score gauge
- Weather summary
- Manual override buttons
- Recent history timeline
Weather Thresholds (in SettingsTab)
Configure automatic transition thresholds:
- Wind warning/severe speeds
- Precipitation thresholds
- Playability warning level
- Auto-block toggle
Configuration
| Env Var | Description | Default |
|---|---|---|
WEATHER_CHECK_INTERVAL_MS | Evaluation frequency | 900000 (15 min) |
WEATHER_ALERT_COOLDOWN_MS | Min time between alerts | 3600000 (1 hour) |
COURSE_STATUS_HYSTERESIS | Recovery margin | 10 |
Metrics
| Metric | Description |
|---|---|
course_status_transitions_total | Transitions by type |
course_playability_score | Current score per course |
course_status_duration_seconds | Time in each status |
weather_evaluation_latency | Check execution time |
Troubleshooting
Status Not Updating
- Check
WeatherAlertScheduleris running - Verify weather API connectivity
- Review threshold configuration
- Check for active manual override
Frequent Flapping
- Increase hysteresis margin
- Adjust thresholds
- Review weather data quality
- Consider longer evaluation intervals
Cart Suspension Issues
- Verify
CartInventoryRepositoryconnectivity - Check listener event handling
- Review cart status after manual restore