# 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 --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`. 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 `` 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//{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/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/) |