Skip to main content

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

StatusDescriptionBookingsCart Policy
OPENNormal operationsAllowedNormal
CONDITIONALLimited play, weather advisoryAllowed with warningMay be restricted
CLOSEDCourse unplayableBlockedSuspended

Playability Score

Weather conditions are converted to a 0-100 playability score:

ScoreLevelStatus Trigger
80-100ExcellentOPEN
60-79GoodOPEN
40-59FairCONDITIONAL (if < warning threshold)
20-39PoorCONDITIONAL
0-19UnplayableCLOSED (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

  1. Cart Suspension: All cart inventory marked INACTIVE
  2. Reservation Cleanup: Pending cart reservations marked RETURNED
  3. Player Notification: Affected bookings notified (if enabled)
  4. WebSocket Broadcast: course.status.changed event emitted
  5. 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)

  1. Cart Resumption: Cart inventory restored to ACTIVE
  2. WebSocket Broadcast: Status change event
  3. 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

MethodEndpointDescription
GET/admin/courses/:courseId/statusGet current status
POST/admin/courses/:courseId/statusSet status (manual)
GET/admin/courses/:courseId/status/historyGet audit log
GET/v1/courses/:courseId/conditionsPublic status (player-facing)

WebSocket Events

EventPayload
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 VarDescriptionDefault
WEATHER_CHECK_INTERVAL_MSEvaluation frequency900000 (15 min)
WEATHER_ALERT_COOLDOWN_MSMin time between alerts3600000 (1 hour)
COURSE_STATUS_HYSTERESISRecovery margin10

Metrics

MetricDescription
course_status_transitions_totalTransitions by type
course_playability_scoreCurrent score per course
course_status_duration_secondsTime in each status
weather_evaluation_latencyCheck execution time

Troubleshooting

Status Not Updating

  1. Check WeatherAlertScheduler is running
  2. Verify weather API connectivity
  3. Review threshold configuration
  4. Check for active manual override

Frequent Flapping

  1. Increase hysteresis margin
  2. Adjust thresholds
  3. Review weather data quality
  4. Consider longer evaluation intervals

Cart Suspension Issues

  1. Verify CartInventoryRepository connectivity
  2. Check listener event handling
  3. Review cart status after manual restore