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

13 KiB

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
  • Business rules (schema, payments, escrow, verification) → product/. 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.slnzero 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 records; 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.

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
Adding a feature — command, query, handler, validator, controller docs/rules/server/cqrs.md
EF Core, audit, state machines, uniqueness, snapshots, search, jobs, seeders docs/rules/server/persistence.md
Anything on the money path — ledger, refunds, BNPL, payouts, invoices docs/rules/server/money.md
Auth, JWE, sessions, field encryption, tenancy, disclosure, logging docs/rules/server/identity.md
C# style, naming, async, error handling, tests, DI docs/rules/server/conventions.md
The wire contract — envelope, status codes, enums, pagination docs/integration/
What is built, what is mocked, what is next docs/status/
Cross-project rules — naming, gates, code quality, config docs/rules/shared/