# 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)
- Current product truth (what to test, what's blocking, what's missing) → [`mvp/`](../mvp/README.md)
- Business rules in depth (archived reference, not actively maintained) → [`archive/product/`](../archive/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 [`mvp/blockers.md`](../mvp/blockers.md).
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`](../archive/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](../archive/docs/rules/server/structure.md) |
| Adding a feature — command, query, handler, validator, controller | [docs/rules/server/cqrs.md](../archive/docs/rules/server/cqrs.md) |
| EF Core, audit, state machines, uniqueness, snapshots, search, jobs, seeders | [docs/rules/server/persistence.md](../archive/docs/rules/server/persistence.md) |
| **Anything on the money path** — ledger, refunds, BNPL, payouts, invoices | [docs/rules/server/money.md](../archive/docs/rules/server/money.md) |
| Auth, JWE, sessions, field encryption, tenancy, disclosure, logging | [docs/rules/server/identity.md](../archive/docs/rules/server/identity.md) |
| C# style, naming, async, error handling, tests, DI | [docs/rules/server/conventions.md](../archive/docs/rules/server/conventions.md) |
| The wire contract — envelope, status codes, enums, pagination | [docs/integration/](../archive/docs/integration/index.md) |
| What is built, what is mocked, what is next | [docs/status/](../archive/docs/status/index.md) |
| Cross-project rules — naming, gates, code quality, config | [docs/rules/shared/](../archive/docs/rules/shared/) |
