# Phase 1: Solution Skeleton + Domain Model **Status:** ⬜ Not Started **Parent plan:** [PLAN.md](./PLAN.md) **Domain:** backend ## Objective Create the .NET 8 solution structure (5 source projects + 4 test projects) and implement the core domain model — entities, value objects, enums, and invariants — with no external dependencies. This establishes the foundation that all later phases reference. ## Tasks - [ ] Create `Marathon.sln` with these projects: - `src/Marathon.Domain/Marathon.Domain.csproj` (classlib, .NET 8, no deps) - `src/Marathon.Application/Marathon.Application.csproj` (classlib, refs Domain) - `src/Marathon.Infrastructure/Marathon.Infrastructure.csproj` (classlib, refs Domain + Application) - `src/Marathon.UI/Marathon.UI.csproj` (Razor Class Library, refs Domain + Application) - `src/Marathon.Hosts.WpfBlazor/Marathon.Hosts.WpfBlazor.csproj` (WPF + BlazorWebView, refs Marathon.UI + Marathon.Infrastructure + Marathon.Application) - `tests/Marathon.Domain.Tests/Marathon.Domain.Tests.csproj` (xUnit) - `tests/Marathon.Application.Tests/Marathon.Application.Tests.csproj` (xUnit) - `tests/Marathon.Infrastructure.Tests/Marathon.Infrastructure.Tests.csproj` (xUnit) - `tests/Marathon.UI.Tests/Marathon.UI.Tests.csproj` (bUnit + xUnit) - [ ] Add `Directory.Build.props` at repo root with shared settings: ```xml net8.0 enable enable 12 true latest ``` - [ ] Add `Directory.Packages.props` for centralized NuGet versions (mark `true`). - [ ] Add `.editorconfig` at repo root with C# formatting rules consistent with CLAUDE.md conventions (file-scoped namespaces, 4-space indent, etc.). - [ ] Implement `Marathon.Domain` types: - **Value objects (records):** - `SportCode(int Value)` — must be > 0 - `EventId(string Value)` — bookmaker's event identifier (string, not int) - `Side` enum: `Side1, Side2, Draw, Less, More` - `BetScope` discriminated union: `Match | Period(int Number)` (use record hierarchy) - `BetType` enum: `Win, Draw, WinFora, Total` - `OddsRate(decimal Value)` — must be > 1.0 - `OddsValue(decimal Value)` — handicap or total threshold (e.g., -5.5, 220.5) - **Entities (use records or classes with private setters as appropriate):** - `Sport(SportCode Code, string NameRu, string NameEn)` - `Country(string Code, string NameRu, string NameEn)` - `League(string Id, SportCode Sport, string Country, string NameRu, string NameEn, string Category)` - `Event(EventId Id, SportCode Sport, string CountryCode, string LeagueId, string Category, DateTimeOffset ScheduledAt, string Side1Name, string Side2Name)` - `Bet(BetScope Scope, BetType Type, Side Side, OddsValue? Value, OddsRate Rate)` - `OddsSnapshot(EventId EventId, DateTimeOffset CapturedAt, OddsSource Source, IReadOnlyList Bets)` where `OddsSource = PreMatch | Live` - `EventResult(EventId EventId, int Side1Score, int Side2Score, Side WinnerSide, DateTimeOffset CompletedAt)` - `Anomaly(Guid Id, EventId EventId, DateTimeOffset DetectedAt, AnomalyKind Kind, decimal Score, string EvidenceJson)` where `AnomalyKind = SuspensionFlip` - [ ] Implement domain invariants in record constructors / static factory methods. - [ ] Implement `Marathon.Domain.Tests` — TDD tests for invariants: - `OddsRate` rejects ≤ 1.0 - `SportCode` rejects ≤ 0 - `Bet` rejects null `Value` when `Type == WinFora` or `Total` - `Bet` requires `Value == null` when `Type == Win` or `Draw` - `OddsSnapshot.Bets` is non-empty - `Event.ScheduledAt` is UTC (`Offset == TimeSpan.Zero`) - Domain types are immutable (no settable public properties) ## Files to Modify/Create - `Marathon.sln` - `Directory.Build.props` - `Directory.Packages.props` - `.editorconfig` - `src/Marathon.Domain/**` — entities, VOs, enums, invariants - `src/Marathon.Application/Marathon.Application.csproj` — empty stub csproj - `src/Marathon.Infrastructure/Marathon.Infrastructure.csproj` — empty stub - `src/Marathon.UI/Marathon.UI.csproj` — empty RCL stub - `src/Marathon.Hosts.WpfBlazor/Marathon.Hosts.WpfBlazor.csproj` — empty stub - `tests/Marathon.Domain.Tests/**` — invariant tests - `tests/Marathon.{Application,Infrastructure,UI}.Tests/*.csproj` — empty xUnit stubs ## Acceptance Criteria - `dotnet build Marathon.sln` succeeds (compile-only smoke check, allowed in Big Bang). - All domain tests pass (`dotnet test tests/Marathon.Domain.Tests` is allowed even in Big Bang since this is the foundation phase and the test project is self-contained). - Domain types are public, immutable records with invariants enforced in constructors. - No EF Core, scraping, or UI code in this phase. ## Notes - Use file-scoped namespaces and one type per file (except small enum + record groups). - Domain types must NOT reference `System.Net.Http`, EF Core, or any infrastructure. - For the discriminated union `BetScope`, use a record hierarchy: ```csharp public abstract record BetScope { /* private ctor */ } public sealed record MatchScope : BetScope; public sealed record PeriodScope(int Number) : BetScope; ``` Or a single record with a nullable `PeriodNumber` — implementer's choice, document it. - Test framework: xUnit with FluentAssertions. Don't add Mockito/NSubstitute yet (no abstractions to mock in Domain). ## Review Checklist - [ ] Solution builds (`dotnet build`) - [ ] Domain tests all pass - [ ] No external deps in `Marathon.Domain.csproj` except framework packages - [ ] Public API surface is minimal — only what later phases need - [ ] All types follow CLAUDE.md naming/style conventions ## Handoff to Next Phase