Files
baya-monorepo/dev/phases/backend/backend-phase-0.md
T
2026-06-28 21:59:59 +03:30

176 lines
12 KiB
Markdown

# Backend Phase 0 — Foundation, cross-cutting seams & starter cleanup
> **Mission:** turn the inherited starter skeleton into a clean Balinyaar foundation. Remove the demo
> scaffolding, stand up the **REST API surface** the marketplace needs (the baseline is gRPC-only today),
> wire the missing cross-cutting plumbing (rate limiting, request logging, current-user + audit-field
> stamping, PII encryption), and define every **mock-able external dependency as a DI seam** with a
> faithful in-memory implementation so later phases just plug real providers in. No domain tables yet —
> this phase makes the next fifteen phases possible and consistent.
>
> **Track:** backend · **Depends on:** nothing (first phase) · **Unlocks:** every backend phase
> **Before you start, read [`../_shared/agent-operating-rules.md`](../_shared/agent-operating-rules.md).** It is not optional.
---
## 1. Context — where this sits
This is the very first build phase. The server (`server/`, .NET 10, Clean Architecture, CQRS via
**`martinothamar/Mediator`** — *not* MediatR) already ships a working spine you must **keep and build
on**, and some demo scaffolding you must **remove**.
**What already exists (do not rebuild) — confirmed in the codebase:**
- ASP.NET Core Identity + **JWE/JWT** + **phone-OTP (passwordless TOTP)** auth, the dynamic-permission
RBAC system, the CQRS pipeline (`ValidateCommandBehavior`, `MetricsBehaviour`), `OperationResult<T>`,
Mapster, FluentValidation, Serilog, OpenTelemetry/prometheus, health checks, the `BaseController` +
the full MVC filter/versioning/Swagger stack, and `Baya.Tests.Setup` (in-memory SQLite).
- The 12-project Clean-Arch solution `Baya.sln`. Identity tables map to the **`usr`** schema.
**What is starter scaffolding you will remove in this phase:**
- The `Order` demo end-to-end: `Domain/Entities/Order/Order.cs`, `Application/Features/Order/**`,
`Contracts/Persistence/IOrderRepository.cs`, `Persistence/Repositories/OrderRepository.cs`,
`Persistence/Configuration/OrderConfig/`, the `User.Orders` nav, `IUnitOfWork.OrderRepository`,
`OrderGrpcServices` + `OrderGrpcServiceModels.proto` and its wiring.
- The three old migrations + snapshot (`Migrations/2021…`, `2022…`, `2023…`, namespace
`Persistence.Migrations`) — they predate the marketplace and will be regenerated as the new baseline
in **backend-phase-1**.
**Known gaps you will close here:** no HTTP controllers exist (REST surface must be created);
`LoggingBehavior<,>` is defined but never registered; **no rate limiting** is wired despite
`CONVENTIONS.md` §11; OTP delivery is stubbed (handled in b2).
## 2. Required reading (do this first)
- [`../_shared/agent-operating-rules.md`](../_shared/agent-operating-rules.md) and
[`../_shared/backend-conventions-checklist.md`](../_shared/backend-conventions-checklist.md).
- [`server/CLAUDE.md`](../../../server/CLAUDE.md) — *Startup wiring*, *Project map*, *Identity & auth*,
*Persistence*; and [`server/CONVENTIONS.md`](../../../server/CONVENTIONS.md) — §1 routing, §4
controllers, §6 persistence (audit fields, soft delete), §9 logging, §11 security (rate limiting),
§12 service registration.
- [`product/overview/platform-summary.md`](../../../product/overview/platform-summary.md) — the four
ground truths (no cash custody, 10% VAT, full-upfront BNPL, weekly payout) and the **IRR-Rials-always**
rule that shapes the money types you scaffold here.
- [`../../contracts/conventions/api-conventions.md`](../../contracts/conventions/api-conventions.md) and
[`money-and-types.md`](../../contracts/conventions/money-and-types.md) — the envelope/format your REST
surface must honour.
- Read the actual code you'll touch: `Baya.Web.Api/Program.cs`, `Baya.Application/Common/*Behavior*.cs`,
`Baya.WebFramework/BaseController/*`, `Baya.Infrastructure.Persistence/ApplicationDbContext.cs`,
and each project's `ServiceConfiguration/` extension.
## 3. Scope — build this
### 3.1 Remove the starter scaffolding
Delete the `Order` feature/entity/repository/config/gRPC/proto and the three old migrations listed in §1.
Remove every reference (nav property, `IUnitOfWork` member, DI wiring, proto compile). The solution must
build clean afterwards. Do **not** remove Identity/auth/observability — those stay.
### 3.2 Stand up the REST surface
The marketplace is REST/JSON. Create the first versioned controller(s) under `Baya.Web.Api/Controllers/V1/`
following `CONVENTIONS.md` §4 exactly (sealed, `BaseController`, inject `ISender`, `[controller]`/`[action]`
tokens, `base.OperationResult(...)`, `[Display(Description=...)]`). A minimal **health/ping** or
**reference** controller is enough to prove the pipeline end-to-end through Swagger; later phases add the
real controllers. Confirm `MapControllers()` now serves real routes and Swagger renders them.
### 3.3 Close the wiring gaps
- **Register `LoggingBehavior<,>`** in the Application pipeline (alongside the existing behaviors), in the
correct order, so every request is structurally logged (no PII — `CONVENTIONS.md` §9).
- **Rate limiting** (`CONVENTIONS.md` §11): add `AddRateLimiter` via a `ServiceConfiguration/` extension
and `UseRateLimiter()` **before** `UseAuthentication()` in `Program.cs`. Define named policies (a
per-IP fixed-window/token-bucket baseline) ready for auth/OTP/refund/payout endpoints to apply later.
### 3.4 Current-user + audit-field stamping
- Add **`ICurrentUser`** (Application contract) wrapping the HTTP context (user id, roles), registered
**Scoped**, with a null-object for non-HTTP contexts (jobs/tests).
- Add a **SaveChanges interceptor** (or extend the existing `SavingChanges` hook) that stamps
`CreatedAt/ModifiedAt` and `CreatedById/ModifiedById` from `ICurrentUser` on a shared base/interface,
per `CONVENTIONS.md` §6. Define the audit-capable base type (extend the existing `BaseEntity`/
`ITimeModification` rather than inventing a parallel one). The `audit_logs` *table* and the
append-only change log come in **b1** — here you build the *field-stamping* plumbing the interceptor
needs and leave a clean extension point for b1 to add log-row writing.
### 3.5 Define the cross-cutting seams (interfaces + mocks + DI)
Create these **Application-layer interfaces** with real-shaped signatures, an **Infrastructure mock
implementation** each, and **DI registration** via a `ServiceConfiguration/` extension (selected by
config so a real impl swaps in later). Keep amounts as IRR `long`. This phase **introduces** these seams;
later phases reuse them.
- **`IDateTimeProvider`** — `DateTimeOffset UtcNow` (so time is testable; no `DateTime.Now` in handlers).
- **`IFieldEncryptor`** — `string Encrypt(string)` / `string Decrypt(string)` (+ a deterministic
`Hash(string)` for lookups like `iban_hash`). Mock = local symmetric key from config; **never** log
plaintext PII. This is what every encrypted PII column will use.
- **`ICacheService`** — typed get/set/remove with TTL and a `GetOrCreateAsync`. Mock = in-memory
(`IMemoryCache`-backed). The config accessor (b1) and read-heavy queries cache through this; Redis later.
- **`IObjectStorage`** — presigned/streamed put/get/delete keyed by an opaque storage key, returning a
retrievable URL. Mock = local-disk/in-memory store under a scratch path. MinIO/S3 later.
- **`INotificationDispatcher`** — `DispatchAsync(notification)` with a channel concept (in-app now;
SMS/push later). Mock = no-op/log; the real in-app `notifications` write lands in b15. Define the seam
now so emitting domains (booking, payments) can depend on it.
> The **OTP/SMS** seam (`ISmsSender`) is introduced in **b2** (with the auth REST surface), and the
> **search/payment/bnpl/bank/vendor** seams in their phases — do not pre-build those here; just leave the
> registry rows. (Listed in [mocks-registry.md](../../shared-working-context/reports/mocks-registry.md).)
### 3.6 Money & convention guardrails
Add (or confirm) the small shared helpers the money path will rely on: IRR is `long`/`BIGINT`
everywhere; if you add a money value object keep it integer-only with no float path. Document the rule in
`server/CONVENTIONS.md` if not already explicit.
## 4. Mocks & seams in this phase
| Seam | Mock behaviour | Registry |
| --- | --- | --- |
| `IDateTimeProvider` | returns real UTC now (deterministic override in tests) | n/a (not external) |
| `IFieldEncryptor` | local symmetric key from config; passthrough-but-reversible; deterministic hash | update row |
| `ICacheService` | in-memory `IMemoryCache` | update row |
| `IObjectStorage` | local-disk/in-memory blob store | update row |
| `INotificationDispatcher` | log/no-op (in-app write arrives in b15) | update row |
Record each in [`mocks-registry.md`](../../shared-working-context/reports/mocks-registry.md) (seam, file,
what's faked, config keys, how to make real, status 🟡).
## 5. Critical rules you must not get wrong
- **Don't break the working spine.** Identity, JWE auth, dynamic permissions, the CQRS behaviors, and
observability must still work after cleanup. Run the existing tests.
- **Seams live in Application, implementations in Infrastructure.** Never reference Infrastructure from
Application/Domain. Register via `ServiceConfiguration/` extensions called from `Program.cs` — no inline
DI in `Program.cs` (`CONVENTIONS.md` §12).
- **Mock = real interface, fake body.** No `if (mock)` branches in handlers; selection is by registration.
- **`IFieldEncryptor` never leaks plaintext** into logs, exceptions, or non-`AsNoTracking` query
projections of PII.
- **Audit-field stamping is infrastructure, not handler code** — handlers never set `CreatedById` etc.
## 6. Definition of Done
The shared [definition-of-done.md](../_shared/definition-of-done.md), plus:
- [ ] `Order` + the three old migrations are gone; `dotnet build Baya.sln` is clean (zero new warnings);
`dotnet test Baya.sln` passes (existing identity tests still green).
- [ ] At least one real REST controller is reachable through Swagger and returns the `OperationResult`
envelope.
- [ ] `LoggingBehavior` registered; rate limiter wired (policies defined, `UseRateLimiter` placed before
auth).
- [ ] `ICurrentUser` + audit-field interceptor in place; the five seams (§3.5) registered with mocks.
- [ ] The **Project map** in `server/CLAUDE.md` is updated (Order removed; seams/cross-cutting noted);
`CONVENTIONS.md` notes the IRR-`BIGINT` money rule if it wasn't explicit.
## 7. How to test (what a human can verify after this phase)
- `dotnet build Baya.sln` and `dotnet test Baya.sln` — both succeed.
- `dotnet run` the API → open `/swagger` → the new REST controller appears and its endpoint returns a
200 in the standard envelope; the Order endpoints are gone.
- Hit the rate-limited test endpoint past its limit → `429`.
- A unit test proves `IFieldEncryptor.Decrypt(Encrypt(x)) == x` and the audit interceptor stamps
`CreatedAt`/`CreatedById` on a save (use the SQLite test context).
## 8. Hand off & document (close the phase)
- **Docs:** update `server/CLAUDE.md` *Project map* (+ a one-line note on the new seams and where they're
registered). Update `CONVENTIONS.md` only if you established a new rule.
- **Contracts:** no domain API yet — but publish the first `swagger.json` snapshot
([openapi/README.md](../../contracts/openapi/README.md)) so the frontend's f0 can wire its type pipeline
against the envelope shape.
- **Handoff & report:** write `shared-working-context/backend/handoff/after-backend-phase-0.md` (the spine
is clean, REST works, seams exist, what f0/b1 can rely on), append to `backend/STATUS.md`, write
`reports/backend-phase-0-report.md`, and update `reports/mocks-registry.md` (the five rows → 🟡).
- **Memory:** save a `project` memory note for any non-obvious decision (e.g. how the seams are selected
by config, the audit interceptor design) with a `MEMORY.md` pointer.