Files
baya-monorepo/server/CLAUDE.md
T
2026-07-30 02:26:52 +03:30

185 lines
13 KiB
Markdown

# Balinyaar Server
The backend API of **Balinyaar**, a trust-first home-nursing marketplace in Iran. It owns the booking
lifecycle, an escrow-style double-entry ledger, weekly nurse payouts, the nurse verification pipeline, and
every piece of encrypted PII and clinical data on the platform.
> Last verified: 2026-07-30 against commit `d3ec723`.
- Repo-wide context and the frontend → root [CLAUDE.md](../CLAUDE.md)
- Business rules (schema, payments, escrow, verification) → [`product/`](../product/index.md). **Read the
relevant doc before designing an entity, feature, or endpoint** — don't infer a business rule from code.
---
## Stack
- **ASP.NET Core / .NET 10** (`net10.0`), Web API
- **Clean Architecture** (Domain → Application → Infrastructure → API)
- **CQRS** with **Mediator** (`martinothamar/Mediator` — source-generator based, **not** MediatR). Use
`ISender`/`ICommand`/`IQuery`; any prose that says "MediatR" is wrong.
- **EF Core 10** + **SQL Server** (Repository + Unit of Work)
- **ASP.NET Core Identity** with **JWE** (signed + AES-128-encrypted JWT), phone-OTP, and dynamic permission
authorization
- **Mapster** for mapping, **FluentValidation** for validation, **Serilog** for structured logging
- **OpenTelemetry** (metrics at `/metrics`, tracing, opt-in OTLP), **NSwag** for OpenAPI, **Asp.Versioning**
- **xUnit** + **NSubstitute** for tests
- All NuGet versions are centrally pinned in `Directory.Packages.props`
## Commands (run from `server/`)
| Task | Command |
| --- | --- |
| Restore / build | `dotnet restore Baya.sln` · `dotnet build Baya.sln` |
| Run API | `dotnet run --project src/API/Baya.Web.Api/Baya.Web.Api.csproj` |
| Test | `dotnet test Baya.sln` |
| Apply migrations (deploy-time one-shot) | `dotnet run --project src/API/Baya.Web.Api/Baya.Web.Api.csproj -- migrate` |
| Add migration | `dotnet ef migrations add <Name> --project src/Infrastructure/Baya.Infrastructure.Persistence --startup-project src/API/Baya.Web.Api` |
| Update DB | `dotnet ef database update --project src/Infrastructure/Baya.Infrastructure.Persistence --startup-project src/API/Baya.Web.Api` |
**Default URL: `http://localhost:5002`** (per `launchSettings.json`), Swagger at `/swagger`. A reachable SQL
Server is required to start.
## Quality gates
1. `dotnet build Baya.sln`**zero new warnings.** Unused usings, locals, parameters, private fields or
members count as failures. Delete them; don't suppress them.
2. `dotnet test Baya.sln` — all tests pass, including the ones your change adds.
3. Read your own diff as if reviewing a PR: would a senior engineer approve it without comment?
4. If the change alters the architecture, update the **Project map** below in the same change.
Two pre-existing warnings are expected and must **not** be "fixed" unless a task says so: `NU1510` on
`Microsoft.Extensions.Logging.Debug` (`Baya.Web.Api`), and `NETSDK1057` (the .NET 10 SDK is preview here).
---
## Hard rules
1. **Dependencies point inward.** Domain references nothing; Application references only Domain.
**Never reference Infrastructure or the API from Domain or Application.**
2. **Never throw for an expected failure.** Return `OperationResult.SuccessResult` / `FailureResult` /
`NotFoundResult` / `ConflictResult`. Let genuinely unexpected exceptions reach the global
`ExceptionHandler`; never swallow one.
3. **Controllers are `sealed`, inherit `BaseController`, inject `ISender`, and return
`base.OperationResult(result)`.** Never call `Ok()`/`BadRequest()`/`NotFound()` directly. One `Send`, one
result, no business logic.
4. **Route segments come from `[controller]`/`[action]` tokens** (the snake_case transformer). Never hardcode a
route string — it also breaks the dynamic-permission key. If a method name doesn't read as a URL, rename it.
5. **Handlers are `internal sealed`; requests are `record`s; one handler per request.** Entities are `class`
with **no public setters**.
6. **Reads use `AsNoTracking()` and project with `.Select()` to a DTO.** Never hydrate entities to map them,
never return an entity from a handler. **Every unbounded list is paginated.**
7. **Access the DB through `IUnitOfWork`; commit once per command.** `ApplicationDbContext` is referenced
directly only inside Infrastructure.
8. **Every soft-deletable entity declares a global query filter** in its `IEntityTypeConfiguration<T>`. A
missing filter is a silent data leak. Never `Where(x => !x.IsDeleted)` per query.
9. **Money is IRR `BIGINT`, integer-only — no float path anywhere.** `gross = commission + payout` always.
Toman converts only inside a provider adapter at its boundary. `ledger_entries` is append-only and every
posting group balances.
10. **Config is rows, read at compute time** via `IPlatformConfig` — never hardcoded. And **a rate change is
never retroactive**: snapshot the rate onto the row at compute time.
11. **Money-path writes are idempotent**: upsert the webhook event first and no-op on a duplicate, claim before
executing, and treat a unique-violation on confirm as an idempotent success. The DB constraint is the
authoritative backstop, not the handler's `if`.
12. **Money movement stays human-approved.** A scheduled job may *generate* a draft payout batch; the
irreversible `process` step is always an explicit admin action.
13. **Route every status write through the forward-only transition table.** `status` has a private setter and
only cohesive domain methods mutate it; the handler pre-checks and returns a clean **409**.
14. **A guarded cross-aggregate flip is one transaction**: load both tracked, mutate through one pure domain
helper, `CommitAsync` once. Never flip a derived flag from a controller or out of band.
15. **Self-committing facades run *after* `CommitAsync()`**`RaiseAsync`, `DispatchAsync`, `WriteAsync` and
`SetConfig` each call `SaveChanges` on the shared scoped context and will flush your partial changes.
16. **`Seams:FieldEncryption:Key` and `:HashKey` are load-bearing — never change them.** They decrypt all
existing PII and derive the phone-lookup hash.
17. **PII goes through `IFieldEncryptor`; equality lookups go through the deterministic hash column.**
**Never query `PhoneNumber == x`.** The encryptor must stay a process-wide singleton.
18. **Two-stage clinical disclosure.** A booking request exposes only limited unencrypted `customer_notes` and
masks the address to a coarse city/district; encrypted care instructions are readable only
post-confirmation, only by the assigned nurse and admin, and never projected into a list or logged.
19. **`is_internal` is a hard visibility boundary enforced at the QUERY layer**, never in the UI. A non-staff
caller can never set or read one.
20. **Tenancy is resolved from `ICurrentUser`, never from the request body, and a mismatch is a clean 404**
never a 403, which confirms the row exists.
21. **Auth, OTP and money endpoints are rate-limited** (`otp` / `auth` / `sensitive` / `webhook` policies).
22. **Never hardcode a secret in C#**, and never put a real value in the base `appsettings.json` — it stays at
its `StartupSecretsGuard`-rejected placeholder. **`dotnet user-secrets` is not used and is not read** (the
`<UserSecretsId>` was removed), so any instruction to use it is stale.
23. **Never concatenate raw SQL.** EF parameterizes; if you must, `FromSqlInterpolated`, never `FromSqlRaw`
with user data.
24. **`async`/`await` all the way, `CancellationToken` threaded through every call.** Never `.Result`,
`.Wait()`, or `async void`. Don't add `.ConfigureAwait(false)` in this app.
25. **Never log PII or secrets.** Structured templates only; use `userId`, not an email.
26. **Package versions live only in `Directory.Packages.props`** — never `Version=` in a `.csproj`.
27. **Register infrastructure through a `ServiceConfiguration/` extension method** called from `Program.cs`.
No inline registration; `Program.cs` stays an orchestrator.
28. **A mock lives behind a DI-registered seam, selected by config, defaulting to the mock.** Never an
`if (mock)` in a handler. Record every mock in `docs/status/`.
29. **No dead code** (the gate is zero new warnings) and **comment the *why*, never the *what*.**
30. **When you change the architecture, update the Project map below in the same change.**
---
## Project map
The canonical list of projects, layers, and cross-layer dependencies — **14 `.csproj` projects, 55 V1
controllers.** Expanded, with the seam catalogue and startup wiring, in
[`docs/rules/server/structure.md`](../docs/rules/server/structure.md).
```
src/
├── Core/
│ ├── Baya.Domain Entities per area (User, Identity, Geography, Catalog, Verification,
│ │ Search, Booking, Payments, Refunds, Invoices, Bnpl, Payouts, Reviews,
│ │ Messaging, PartnerCenters, + Configuration/Audit/Analytics/Holidays/
│ │ Notifications/SupportAlerts) · Common/ (BaseEntity, IAuditable,
│ │ [AuditRedacted]) · status-code sets + transition tables
│ └── Baya.Application Features/<Area>/{Commands|Queries}/ · Contracts/ (the seams:
│ Common, Payments, Search, Reviews, Persistence) · Models/ ·
│ pipeline behaviors (Logging → Metrics → Validate)
├── Infrastructure/
│ ├── Baya.Infrastructure.Persistence ApplicationDbContext (+ encrypted-PII converters & phone-hash sync) ·
│ │ ValueConversion/ · Configuration/<Area>Config/ · Repositories/ ·
│ │ Migrations/ · Interceptors/ (AuditFieldInterceptor) ·
│ │ Services/ (DB-backed platform facades, Scheduling/, Search/, Seeding/)
│ ├── Baya.Infrastructure.Identity Jwt/ · Identity/ (Managers, Stores, PermissionManager, Seed, CurrentUser/)
│ ├── Baya.Infrastructure.CrossCutting Serilog wiring · Seams/ (mocks) · Seams/Real/ (vendor adapters) ·
│ │ AddCrossCuttingSeams
│ └── Baya.Infrastructure.Monitoring HealthChecks (live/ready split) · OpenTelemetry
├── API/
│ ├── Baya.Web.Api Program.cs · Controllers/V1/ (55) · appsettings*.json
│ ├── Baya.WebFramework BaseController · Filters/ · Middlewares/ · Swagger/ · Routing/ ·
│ │ ServiceConfiguration/ (rate limiting)
│ └── Plugins/Baya.Web.Plugins.Grpc gRPC services + .proto (User only)
├── Shared/Baya.SharedKernel Extensions + validation base
└── Tests/
├── Baya.Tests.Setup Shared test infrastructure (SQLite, NSubstitute, TestFieldEncryptor)
├── Baya.Test.Infrastructure.Identity xUnit identity tests
├── Baya.Test.Foundation Cross-cutting plumbing + identity handler unit tests
└── Baya.Test.Api WebApplicationFactory integration tests (in-memory SQLite, env "Testing")
```
**DB schemas**, one per area: `usr`, `ops`, `geo`, `catalog`, `verif`, `search`, `booking`, `payments`,
`payouts`, `reviews`, `messaging`, `partner`.
> `Features/Booking` (singular — the money-free pre-payment request) and `Features/Bookings` (plural — the
> post-payment engine) are **different areas, not a rename.** The entity type `Booking` is aliased where the
> namespaces collide.
---
## Where to read more
Open **one** of these for the area you are touching.
| Working on… | Read |
| --- | --- |
| Projects, layers, startup wiring, the seam catalogue, observability | [docs/rules/server/structure.md](../docs/rules/server/structure.md) |
| Adding a feature — command, query, handler, validator, controller | [docs/rules/server/cqrs.md](../docs/rules/server/cqrs.md) |
| EF Core, audit, state machines, uniqueness, snapshots, search, jobs, seeders | [docs/rules/server/persistence.md](../docs/rules/server/persistence.md) |
| **Anything on the money path** — ledger, refunds, BNPL, payouts, invoices | [docs/rules/server/money.md](../docs/rules/server/money.md) |
| Auth, JWE, sessions, field encryption, tenancy, disclosure, logging | [docs/rules/server/identity.md](../docs/rules/server/identity.md) |
| C# style, naming, async, error handling, tests, DI | [docs/rules/server/conventions.md](../docs/rules/server/conventions.md) |
| The wire contract — envelope, status codes, enums, pagination | [docs/integration/](../docs/integration/index.md) |
| What is built, what is mocked, what is next | [docs/status/](../docs/status/index.md) |
| Cross-project rules — naming, gates, code quality, config | [docs/rules/shared/](../docs/rules/shared/) |