Files
maraphon-app/plans/initial-implementation/phase-1-solution-and-domain.md
T

123 lines
6.0 KiB
Markdown

# 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
<Project>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>12</LangVersion>
<TreatWarningsAsErrors Condition="'$(Configuration)'=='Release'">true</TreatWarningsAsErrors>
<AnalysisLevel>latest</AnalysisLevel>
</PropertyGroup>
</Project>
```
- [ ] Add `Directory.Packages.props` for centralized NuGet versions (mark
`<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>`).
- [ ] 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<Bet> 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
<!-- Filled by Phase 1 implementer. -->