# Phase 4: Application Layer + Background Workers **Status:** ⬜ Not Started **Parent plan:** [PLAN.md](./PLAN.md) **Domain:** backend **Depends on:** Phase 1 (Domain), Phase 2 (Storage), Phase 3 (Scraping) ## Objective Wire scraping + storage together via use-case orchestrators in the Application layer and background services that execute pollers on configurable intervals. ## Tasks - [ ] Implement use cases in `Marathon.Application/UseCases/`: - `PullUpcomingEventsUseCase(IOddsScraper, IEventRepository, ISnapshotRepository)` - `ExecuteAsync(CancellationToken)` → fetch upcoming events, persist new ones, capture initial pre-match snapshots for each - `PullLiveOddsUseCase(IOddsScraper, IEventRepository, ISnapshotRepository)` - `ExecuteAsync(CancellationToken)` → for each currently-live event, fetch a fresh snapshot, persist it - `PullResultsUseCase(IOddsScraper, IEventRepository, IResultRepository)` - `ExecuteAsync(DateRange range, IReadOnlyList? selection, CancellationToken)` → fetch results for completed events (all or selected) - `ExportToExcelUseCase(IExcelExporter, IEventRepository)` - `ExecuteAsync(DateRange, ExportKind, CancellationToken)` - [ ] Implement background services in `Marathon.Infrastructure/Workers/`: - `UpcomingEventsPoller : BackgroundService` — runs `PullUpcomingEventsUseCase` on a configurable cron-like schedule (default: every 6 hours) - `LiveOddsPoller : BackgroundService` — runs `PullLiveOddsUseCase` every `Scraping:PollingIntervalSeconds` seconds - Both honor `CancellationToken`, log via `ILogger`, and skip cycles gracefully on errors (don't crash the host) - [ ] Add `WorkerOptions` POCO bound to `Workers:*` config: ```csharp public sealed class WorkerOptions { public string UpcomingScheduleCron { get; init; } = "0 0 */6 * * *"; // every 6h public bool LivePollerEnabled { get; init; } = true; public bool UpcomingPollerEnabled { get; init; } = true; } ``` Use `Cronos` package or simple TimeSpan for upcoming schedule. - [ ] Add DI extension `AddMarathonApplication(IServiceCollection, IConfiguration)` in `Marathon.Application/DependencyInjection.cs`: - Registers all use cases - [ ] Update `Marathon.Infrastructure/DependencyInjection.cs` to also register `BackgroundService`s under `services.AddHostedService()`. - [ ] Tests in `Marathon.Application.Tests`: - Mock `IOddsScraper` + repos with NSubstitute - Test: `PullUpcomingEventsUseCase` persists new events, skips duplicates - Test: `PullLiveOddsUseCase` writes a snapshot per live event - Test: `PullResultsUseCase` respects `selection` filter (when null, fetches all) - Test: `ExportToExcelUseCase` invokes `IExcelExporter.ExportAsync` with correct date range - [ ] Tests in `Marathon.Infrastructure.Tests/Workers/`: - Test: `LiveOddsPoller` invokes use case at configured interval (use FakeTimeProvider) - Test: poller continues after a use-case exception (logs, doesn't propagate) ## Files to Modify/Create - `src/Marathon.Application/UseCases/*.cs` - `src/Marathon.Application/DependencyInjection.cs` - `src/Marathon.Infrastructure/Workers/UpcomingEventsPoller.cs` - `src/Marathon.Infrastructure/Workers/LiveOddsPoller.cs` - `src/Marathon.Infrastructure/Configuration/WorkerOptions.cs` - `tests/Marathon.Application.Tests/UseCases/**` - `tests/Marathon.Infrastructure.Tests/Workers/**` ## Acceptance Criteria - Compiles (Big Bang). - Use cases depend only on Application abstractions (no Infrastructure refs). - Workers honor cancellation and don't crash on transient errors. - All variable timing/enabling is configurable. ## Notes - Use `IHostedService` from `Microsoft.Extensions.Hosting` — works in WPF host via `Host.CreateApplicationBuilder()` pattern (Phase 5 will expose this). - For the cron-style upcoming poller, prefer the `Cronos` package (small, mature) over hand-rolled scheduling. - Big Bang: compile-only smoke check. ## Review Checklist - [ ] Use cases have no Infrastructure dependencies - [ ] Both pollers configurable (interval, enable/disable) - [ ] Cancellation propagated correctly - [ ] Errors logged, not propagated out of `ExecuteAsync` ## Handoff to Next Phase