186 lines
13 KiB
Markdown
186 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)
|
|
- 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/) |
|