# PostRaceScheduler Enhancement - Result Confirmation

## Overview

Enhanced the PostRaceScheduler to intelligently handle the provisional → confirmed result transition that occurs after races are completed.

## Problem Statement

TAB results go through two phases:
1. **Provisional Results** (status: "CLOSED") - Available shortly after race completion, but may change
2. **Confirmed Results** (status: "FINAL") - Official results, typically 10-30 minutes after race

Previously, the scheduler would fetch results once and not detect if they were provisional or confirmed.

## Solution Implemented

### 1. Result Status Detection

The scheduler now checks the race.status field from the API:
- `"FINAL"` or `"CLOSED"` = Confirmed results
- Other status values = Provisional results (need retry)

```typescript
const isConfirmed = updatedRace.status.toUpperCase() === 'FINAL' ||
                   updatedRace.status.toUpperCase() === 'CLOSED';
```

### 2. Automatic Retry for Provisional Results

When provisional results are detected, the scheduler automatically schedules retries:
- **Max Retries**: 3
- **Retry Delays**: 5 minutes, 5 minutes, 10 minutes (20 minutes total)
- **Tracking**: All retries linked via `parentJobRunId` in JobRun table

```typescript
if (provisionalCount > 0 && retryCount < RETRY_CONFIG.POST_RACE.maxRetries) {
  const retryDelayMs = RETRY_CONFIG.POST_RACE.retryDelays[retryCount];
  this.scheduleRetry(retryDelayMs, context.jobRunId, retryCount + 1);
}
```

### 3. Enhanced Metadata Tracking

JobRun metadata now includes:
- `provisionalResults[]` - Races with provisional results
- `confirmedResults[]` - Races with confirmed results
- `retryScheduled` - Boolean indicating if retry was scheduled

Example metadata:
```json
{
  "resultsProcessed": 5,
  "meetingsUpdated": 3,
  "provisionalResults": [
    {
      "raceId": "abc-123",
      "meetingId": "def-456",
      "raceNumber": 3,
      "status": "CLOSED"
    }
  ],
  "confirmedResults": [
    {
      "raceId": "xyz-789",
      "meetingId": "ghi-012",
      "raceNumber": 4
    }
  ],
  "retryScheduled": true
}
```

### 4. Improved Race Query Logic

Updated `findRecentlyCompletedRaces()` to:
- Query races that started 5-20 minutes ago (using `POST_RACE_INITIAL` time window)
- Exclude races already marked as FINAL, ABANDONED, or CANCELLED
- Only check races that still need result confirmation

```typescript
const races = await this.prisma.race.findMany({
  where: {
    startTime: {
      gte: windowStart.toJSDate(),
      lte: windowEnd.toJSDate(),
    },
    status: {
      notIn: ['FINAL', 'ABANDONED', 'CANCELLED'],
    },
  },
});
```

## Configuration

### Retry Configuration (src/schedulers/config.ts)

```typescript
POST_RACE: {
  maxRetries: 3,
  retryDelays: [300000, 300000, 600000], // 5 min, 5 min, 10 min
}
```

### Time Windows (src/schedulers/config.ts)

```typescript
POST_RACE_INITIAL: {
  minutesAfter: 10,
  windowMinutes: 10, // Check races finished 5-20 minutes ago
}
```

## Behavior Flow

1. **Initial Check** (T+5-20 minutes after race start)
   - Race finishes at 2:00 PM
   - Scheduler runs at 2:10 PM
   - Fetches meeting data, finds race status = "CLOSED" (provisional)
   - Records provisional result in metadata
   - Schedules retry for 2:15 PM

2. **First Retry** (T+15 minutes)
   - Scheduler retry runs at 2:15 PM
   - Fetches meeting data again
   - Still provisional? Schedule another retry for 2:20 PM

3. **Second Retry** (T+20 minutes)
   - Scheduler retry runs at 2:20 PM
   - Fetches meeting data again
   - Still provisional? Schedule final retry for 2:30 PM

4. **Final Retry** (T+30 minutes)
   - Scheduler retry runs at 2:30 PM
   - Fetches meeting data again
   - If still provisional, gives up (logged as partial success)
   - If confirmed, marks as success

5. **Success Path**
   - If any check finds status = "FINAL", marks as confirmed
   - No further retries scheduled
   - JobRun marked as success

## Observability

### Logs

```typescript
logger.info({
  meetingId: race.meetingId,
  raceNumber: race.raceNumber,
  status: updatedRace.status,
}, 'Provisional race results - may retry for confirmation');

logger.info({
  provisionalResults: provisionalCount,
  retryCount: retryCount + 1,
  retryDelayMs,
  nextRetryAt: new Date(Date.now() + retryDelayMs).toISOString(),
}, 'Scheduling retry for provisional results');
```

### Database Queries

```sql
-- Find races with provisional results
SELECT
  jr."jobType",
  jr."startedAt",
  jr.metadata->'provisionalResults' as provisional,
  jr.metadata->'retryScheduled' as retry_scheduled
FROM job_runs jr
WHERE
  jr."jobType" = 'post_race_t5'
  AND jr.metadata->>'retryScheduled' = 'true'
ORDER BY jr."startedAt" DESC
LIMIT 10;

-- Track retry chains
SELECT
  jr.id,
  jr."retryCount",
  jr.status,
  jr."parentJobRunId",
  jr."startedAt"
FROM job_runs jr
WHERE
  jr."jobType" = 'post_race_t5'
  AND jr."parentJobRunId" IS NOT NULL
ORDER BY jr."startedAt" DESC;

-- Confirmed vs provisional results
SELECT
  DATE(jr."startedAt") as date,
  COUNT(*) FILTER (WHERE jsonb_array_length(jr.metadata->'confirmedResults') > 0) as confirmed,
  COUNT(*) FILTER (WHERE jsonb_array_length(jr.metadata->'provisionalResults') > 0) as provisional
FROM job_runs jr
WHERE jr."jobType" = 'post_race_t5'
GROUP BY DATE(jr."startedAt")
ORDER BY date DESC;
```

## Benefits

1. **Data Completeness** - Ensures we always get confirmed results, not just provisional
2. **Automatic Recovery** - Handles the variable timing of result confirmation
3. **Full Visibility** - JobRun metadata shows exactly which results are provisional vs confirmed
4. **Retry Intelligence** - Uses increasing delays (5, 5, 10 min) to accommodate typical confirmation times
5. **Database Efficiency** - Only queries races that actually need checking, skips already-confirmed races

## Testing

To test the enhancement:

```bash
# Watch logs for result confirmation
docker compose logs -f app | grep -E "provisional|confirmed|retry"

# Check JobRun metadata
docker exec racing-postgres psql -U racing -d racing_db -c "
  SELECT
    \"jobType\",
    status,
    \"retryCount\",
    \"itemsProcessed\",
    metadata->'provisionalResults' as provisional,
    metadata->'confirmedResults' as confirmed,
    metadata->'retryScheduled' as retry_scheduled
  FROM job_runs
  WHERE \"jobType\" = 'post_race_t5'
  ORDER BY \"startedAt\" DESC
  LIMIT 5;
"
```

## Files Modified

1. **src/schedulers/post-race-scheduler.ts**
   - Added import for RETRY_CONFIG
   - Enhanced metadata structure
   - Implemented result status checking
   - Added automatic retry scheduling
   - Improved race query logic

2. **src/schedulers/pre-race-scheduler.ts**
   - Fixed duplicate prisma property
   - Fixed manual trigger jobRunId

## Success Metrics

| Metric | Before | After |
|--------|--------|-------|
| Result confirmation detection | ❌ None | ✅ Full |
| Provisional result handling | ❌ Manual | ✅ Automatic |
| Retry logic | ❌ None | ✅ 3 retries |
| Observability | ⚠️ Basic | ✅ Detailed |
| Race query efficiency | ⚠️ All races | ✅ Filtered by status |

## Next Steps

This enhancement completes the PostRaceScheduler functionality. Remaining tasks:

1. **PreRaceScheduler Efficiency** - Implement DB-first querying to avoid unnecessary API calls
2. **Grafana Dashboard** - Add panels for provisional vs confirmed result metrics
3. **Unit Tests** - Write tests for retry logic and status detection

---

**Status**: ✅ **COMPLETE**
**Date**: 2026-01-14
**Impact**: Ensures data completeness and handles provisional → confirmed result transitions automatically
