Summary
Replace the legacy Cosmos/Azure Function population path with a scheduled worker that seeds the live Postgres model used by the app.
Initial scope is only the baseline data the web app depends on now: competitions, drivers, and races.
Problem
Current population function is built around old Cosmos-era assumptions.
The app now reads from Postgres via EF.
Race results are still mocked in API/web flow, so “populate results” is a separate phase.
Competition records and race deadlines need deterministic bootstrapping rules.
Goals
Seed app-required baseline data directly into Postgres.
Make ingestion idempotent and safe to rerun.
Define clear competition setup for multi-competition years.
Establish placeholder race deadline policy for v1.
Decommission dependency on legacy population function path.
Out of Scope (for this epic)
Persisted race-results schema and ingestion.
Standings/history persistence.
Constructor/team normalization.
User selections or race metadata authoring flows.
Requirements
Functional
Seed competitions:
Philip 2025 (Year: 2025)
David 2025 (Year: 2025)
Main 2026 (Year: 2026)
Fetch and upsert drivers from Jolpica source.
Fetch and upsert races from Jolpica source.
Set race deadlines for v1 as placeholders:
PreQualyDeadlineUtc = StartTimeUtc - 30 minutes
FinalDeadlineUtc = StartTimeUtc - 30 minutes
Maintain idempotency on repeated runs.
Non-Functional
No duplicate rows on reruns.
Clear logging and failure visibility.
Safe partial-failure behavior (retry/restart without corruption).
No secrets in logs.
Epic Acceptance Criteria
Worker can run end-to-end and populate Competitions, Drivers, and Races in Postgres.
Running the worker twice produces no duplicate competitions, drivers, or races.
Races are linked to valid competition records (no FK errors).
Deadlines are populated with the agreed placeholder policy.
Existing web flows dependent on drivers/races continue to work.
Legacy function path is disabled/deprioritized for this data path.
Story Breakdown
Story 1: Worker Skeleton + Runtime
Create scheduled worker/console job project.
Wire config, logging, and execution entrypoint.
Add DB and external API connectivity setup.
Done when: Worker runs in target environment with config validation.
Story 2: Competition Seeding
Seed or upsert Philip 2025, David 2025, Main 2026.
Ensure deterministic lookup/matching and no duplicates.
Done when: Competitions exist and reruns are stable.
Story 3: Driver Ingestion
Fetch season driver data.
Upsert into Postgres keyed by DriverId.
Done when: Driver table reflects source set, reruns idempotent.
Story 4: Race Ingestion + Deadline Placeholders
Fetch race schedule.
Map and upsert race records.
Apply 30-minute-before-start placeholders for both deadlines.
Link to seeded competition IDs.
Done when: Race table populated with stable keys/FKs/deadlines.
Story 5: Operational Hardening
Add retries/error handling around external API calls.
Add run summary metrics/logging.
Document runbook and rollback behavior.
Done when: Operational behavior is predictable and supportable.
Story 6: Cutover/Decommission
Stop relying on legacy population function path for seeded baseline data.
Keep migration notes and fallback instructions.
Done when: New worker is the canonical seed mechanism.
Risks and Mitigations
Risk: Competition mapping ambiguity for races.
Mitigation: Explicit mapping config and validation on startup.
Risk: Deadline placeholder causes UX confusion.
Mitigation: Document as temporary policy; include follow-up story for real deadline logic.
Risk: External API instability/rate limits.
Mitigation: Backoff/retry + run resumability.
Risk: Scope creep into results persistence.
Mitigation: Keep results in a separate epic/phase.
Phase 2 (Follow-on Epic)
Persisted race results and standings:
Add race-results table/model/repository/API path.
Replace mocked /races/results.
Extend worker to ingest completed race results.
Optionally include constructor/team model if needed.
Summary
Replace the legacy Cosmos/Azure Function population path with a scheduled worker that seeds the live Postgres model used by the app.
Initial scope is only the baseline data the web app depends on now: competitions, drivers, and races.
Problem
Current population function is built around old Cosmos-era assumptions.
The app now reads from Postgres via EF.
Race results are still mocked in API/web flow, so “populate results” is a separate phase.
Competition records and race deadlines need deterministic bootstrapping rules.
Goals
Seed app-required baseline data directly into Postgres.
Make ingestion idempotent and safe to rerun.
Define clear competition setup for multi-competition years.
Establish placeholder race deadline policy for v1.
Decommission dependency on legacy population function path.
Out of Scope (for this epic)
Persisted race-results schema and ingestion.
Standings/history persistence.
Constructor/team normalization.
User selections or race metadata authoring flows.
Requirements
Functional
Seed competitions:
Philip 2025 (Year: 2025)
David 2025 (Year: 2025)
Main 2026 (Year: 2026)
Fetch and upsert drivers from Jolpica source.
Fetch and upsert races from Jolpica source.
Set race deadlines for v1 as placeholders:
PreQualyDeadlineUtc = StartTimeUtc - 30 minutes
FinalDeadlineUtc = StartTimeUtc - 30 minutes
Maintain idempotency on repeated runs.
Non-Functional
No duplicate rows on reruns.
Clear logging and failure visibility.
Safe partial-failure behavior (retry/restart without corruption).
No secrets in logs.
Epic Acceptance Criteria
Worker can run end-to-end and populate Competitions, Drivers, and Races in Postgres.
Running the worker twice produces no duplicate competitions, drivers, or races.
Races are linked to valid competition records (no FK errors).
Deadlines are populated with the agreed placeholder policy.
Existing web flows dependent on drivers/races continue to work.
Legacy function path is disabled/deprioritized for this data path.
Story Breakdown
Story 1: Worker Skeleton + Runtime
Create scheduled worker/console job project.
Wire config, logging, and execution entrypoint.
Add DB and external API connectivity setup.
Done when: Worker runs in target environment with config validation.
Story 2: Competition Seeding
Seed or upsert Philip 2025, David 2025, Main 2026.
Ensure deterministic lookup/matching and no duplicates.
Done when: Competitions exist and reruns are stable.
Story 3: Driver Ingestion
Fetch season driver data.
Upsert into Postgres keyed by DriverId.
Done when: Driver table reflects source set, reruns idempotent.
Story 4: Race Ingestion + Deadline Placeholders
Fetch race schedule.
Map and upsert race records.
Apply 30-minute-before-start placeholders for both deadlines.
Link to seeded competition IDs.
Done when: Race table populated with stable keys/FKs/deadlines.
Story 5: Operational Hardening
Add retries/error handling around external API calls.
Add run summary metrics/logging.
Document runbook and rollback behavior.
Done when: Operational behavior is predictable and supportable.
Story 6: Cutover/Decommission
Stop relying on legacy population function path for seeded baseline data.
Keep migration notes and fallback instructions.
Done when: New worker is the canonical seed mechanism.
Risks and Mitigations
Risk: Competition mapping ambiguity for races.
Mitigation: Explicit mapping config and validation on startup.
Risk: Deadline placeholder causes UX confusion.
Mitigation: Document as temporary policy; include follow-up story for real deadline logic.
Risk: External API instability/rate limits.
Mitigation: Backoff/retry + run resumability.
Risk: Scope creep into results persistence.
Mitigation: Keep results in a separate epic/phase.
Phase 2 (Follow-on Epic)
Persisted race results and standings:
Add race-results table/model/repository/API path.
Replace mocked /races/results.
Extend worker to ingest completed race results.
Optionally include constructor/team model if needed.