cleanup phase 7
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
---
|
||||
name: backend-feature
|
||||
description: >-
|
||||
Add a feature to the Balinyaar .NET server — a command, a query, or both — end to end: the Application
|
||||
slice, the controller, EF configuration/migration if it touches a table, tests, and the doc updates it
|
||||
triggers. Use when implementing a new endpoint or extending an existing one anywhere under server/.
|
||||
---
|
||||
|
||||
# Balinyaar Backend Feature
|
||||
|
||||
The sequence for shipping one CQRS slice, from the Application layer to a green gate.
|
||||
|
||||
**Precedence.** This skill is the **procedure** — what order to do things in. The **rules** within each
|
||||
step live in `docs/rules/server/` and this skill defers to them; it doesn't restate them.
|
||||
|
||||
| For | Read |
|
||||
|-----|------|
|
||||
| The dispatcher, folder shape, `OperationResult`, the controller skeleton, authorization | [docs/rules/server/cqrs.md](../../../docs/rules/server/cqrs.md) |
|
||||
| Projects, layers, the seam catalogue, startup wiring | [docs/rules/server/structure.md](../../../docs/rules/server/structure.md) |
|
||||
| EF Core, migrations, soft-delete, audit, config-as-rows, state machines, snapshots, uniqueness | [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 | [docs/rules/server/identity.md](../../../docs/rules/server/identity.md) |
|
||||
| C# style, naming, async, testing | [docs/rules/server/conventions.md](../../../docs/rules/server/conventions.md) |
|
||||
| The gate, and what "done" means | [docs/rules/shared/git-and-gates.md](../../../docs/rules/shared/git-and-gates.md) |
|
||||
|
||||
**Stack:** ASP.NET Core (.NET 10), Clean Architecture, CQRS on `martinothamar/Mediator` (a source generator —
|
||||
**not MediatR**; there is no `IMediator` anywhere in this codebase), EF Core, FluentValidation, Mapster.
|
||||
|
||||
---
|
||||
|
||||
## 1. Scope it before writing anything
|
||||
|
||||
- **Which area?** The Application feature areas mirror the Domain entity folders — `Identity`, `Geography`,
|
||||
`Catalog`, `Verification`, `Search`, `Booking` (singular, pre-payment) / `Bookings` (plural, post-payment),
|
||||
`Payments`, `Refunds`, `Invoices`, `Bnpl`, `Payouts`, `Reviews`, `PatientCareRecords`, `Messaging`,
|
||||
`PartnerCenters`, `Configuration`, `Audit`, `Analytics`, `Holidays`, `Notifications`, `SupportAlerts`. Full
|
||||
list and the schema-per-area mapping: [structure.md](../../../docs/rules/server/structure.md) §2.
|
||||
- **Command or query, or both?** A command mutates; a query reads. Most features are a matched pair (create
|
||||
+ get, or update + list).
|
||||
- **Find a sibling to mirror.** Grep the area's existing folder —
|
||||
`Features/<Area>/{Commands,Queries}/` — for a feature shaped like the one you're adding. Copying a live
|
||||
pattern beats inventing a new one.
|
||||
- **Does it touch money?** (ledger, refunds, invoices, BNPL, payouts) → read
|
||||
[money.md](../../../docs/rules/server/money.md) **first**. The invariants there (integer IRR, balanced
|
||||
ledger postings, webhook idempotency, snapshot-at-compute-time) are not suggestions.
|
||||
- **Does it add or change a table?** → read [persistence.md](../../../docs/rules/server/persistence.md) §5–7
|
||||
before modeling it (soft-delete filters, forward-only status machines, snapshot fields, uniqueness
|
||||
patterns all have a house pattern — don't reinvent one).
|
||||
- **Does it need a new external dependency** (a vendor, a rail)? It becomes an interface in
|
||||
`Application/Contracts/`, mock in `CrossCutting/Seams/`, real in `CrossCutting/Seams/Real/`, selected by a
|
||||
`Seams:<rail>:Provider` config key that **falls closed to the mock**. See
|
||||
[structure.md](../../../docs/rules/server/structure.md) §3.
|
||||
|
||||
---
|
||||
|
||||
## 2. The Application slice
|
||||
|
||||
```
|
||||
Baya.Application/Features/<Area>/
|
||||
├── Commands/<VerbNoun>Command/
|
||||
│ ├── <VerbNoun>Command.cs record : IRequest<OperationResult<T>>
|
||||
│ ├── <VerbNoun>Command.Handler.cs internal sealed class : IRequestHandler<…>
|
||||
│ └── <VerbNoun>Command.Validator.cs AbstractValidator<Command> (omit when there is nothing to validate)
|
||||
└── Queries/<VerbNoun>Query/
|
||||
├── <VerbNoun>Query.cs
|
||||
├── <VerbNoun>Query.Handler.cs
|
||||
└── <VerbNoun>Query.Result.cs record Result(…) ← the DTO returned
|
||||
```
|
||||
|
||||
1. Create the folder, one type per file, file name matching the type name.
|
||||
2. The request is a `record`; the handler is `internal sealed`; return `OperationResult<T>` — never throw
|
||||
for an expected failure (`SuccessResult`/`FailureResult`/`NotFoundResult`/`ConflictResult` map to
|
||||
200/400/404/409). Let a genuinely unexpected exception propagate to the global `ExceptionHandler`.
|
||||
3. Add a FluentValidation validator if the request takes input. **Never validate a route-supplied id in the
|
||||
body command** — route values aren't bound into it.
|
||||
4. Query: `AsNoTracking()` + `.Select()` straight to the DTO — never hydrate an entity graph to map it in
|
||||
memory. Command: use `Include` only when you need navigation properties loaded to mutate the aggregate,
|
||||
access the DB through `IUnitOfWork`, and `CommitAsync` once at the end.
|
||||
|
||||
Full rules and the validator/OperationResult examples: [cqrs.md](../../../docs/rules/server/cqrs.md) §1–3.
|
||||
|
||||
---
|
||||
|
||||
## 3. Persistence — only if you added or changed a table
|
||||
|
||||
1. One `IEntityTypeConfiguration<T>` in `Persistence/Configuration/<Area>Config/`.
|
||||
2. A soft-deletable entity **must** declare `HasQueryFilter(o => !o.IsDeleted)` — without it, deleted rows
|
||||
leak into every query that doesn't explicitly exclude them.
|
||||
3. A lifecycle `status` column is a forward-only machine: `const string` codes, a private setter, cohesive
|
||||
transition methods, a static allowed-edges table. The handler pre-checks and returns a clean `409` — it
|
||||
never throws for "already moved."
|
||||
4. A row that represents a past agreement (a price, an address, a policy, a deadline) is a **snapshot** —
|
||||
frozen at compute time, never re-derived from a later edit to its source.
|
||||
5. Money-critical constants (rates, deadlines, tolerances) are read via `IPlatformConfig.GetConfig<T>` —
|
||||
**never hardcoded**, and never re-read for an already-priced row.
|
||||
|
||||
```bash
|
||||
dotnet ef migrations add <Name> --project src/Infrastructure/Baya.Infrastructure.Persistence --startup-project src/API/Baya.Web.Api
|
||||
dotnet ef database update --project src/Infrastructure/Baya.Infrastructure.Persistence --startup-project src/API/Baya.Web.Api
|
||||
```
|
||||
|
||||
Full patterns, with the exact uniqueness/snapshot/state-machine tables:
|
||||
[persistence.md](../../../docs/rules/server/persistence.md).
|
||||
|
||||
---
|
||||
|
||||
## 4. The controller
|
||||
|
||||
```csharp
|
||||
[ApiVersion("1")]
|
||||
[ApiController]
|
||||
[Route("api/v{version:apiVersion}/[controller]")]
|
||||
[Display(Description = "One-line description shown in Swagger")]
|
||||
[Authorize(ConstantPolicies.DynamicPermission)] // or [Authorize], or omit for public
|
||||
public sealed class MyFeatureController(ISender sender) : BaseController
|
||||
{
|
||||
[HttpPost("[action]")]
|
||||
[ProducesOkApiResponseType<MyCommandResult>]
|
||||
public async Task<IActionResult> CreateSomething(MyCommand command, CancellationToken ct)
|
||||
=> OperationResult(await sender.Send(command, ct));
|
||||
}
|
||||
```
|
||||
|
||||
- `sealed`, inject `ISender` via the primary constructor, always `base.OperationResult(result)` — never
|
||||
`Ok()`/`BadRequest()`/`NotFound()` directly.
|
||||
- Never hardcode a route string. If the method name doesn't read cleanly as the URL segment
|
||||
`SnakeCaseParameterTransformer` will produce, rename the method instead.
|
||||
- Pick the narrowest authorization that fits: none (truly public) → `[Authorize]` (any authenticated user) →
|
||||
`[Authorize(ConstantPolicies.DynamicPermission)]` (role/claim-gated admin action). Table and rate-limiting
|
||||
notes: [cqrs.md](../../../docs/rules/server/cqrs.md) §4.
|
||||
|
||||
---
|
||||
|
||||
## 5. Tests
|
||||
|
||||
1. **Handler unit test** (xUnit + NSubstitute + FluentAssertions), Arrange-Act-Assert, named
|
||||
`{MethodUnderTest}_{Scenario}_{ExpectedOutcome}`. Test the handler directly, not the controller.
|
||||
2. **At least one `WebApplicationFactory<Program>` integration test** in `Baya.Test.Api` for the area,
|
||||
covering: happy path → 200, unauthenticated → 401, validation failure → 400 with field detail.
|
||||
3. The recurring-job scheduler is dormant under `Testing`, so a background tick can't make an integration
|
||||
test flaky — you don't need to account for it.
|
||||
|
||||
Examples and the full testing convention: [conventions.md](../../../docs/rules/server/conventions.md) §8.
|
||||
|
||||
---
|
||||
|
||||
## 6. Docs this feature triggers — in the same change
|
||||
|
||||
- **[`docs/integration/domains/<domain>.md`](../../../docs/integration/domains/index.md)** — add the new
|
||||
endpoint with its verdict (`wired`/`unwired`/`phantom`), matching the client `services/` domain it belongs
|
||||
to.
|
||||
- **The OpenAPI snapshot** — regenerate `docs/integration/openapi/swagger.v1.json` per
|
||||
[openapi/README.md](../../../docs/integration/openapi/README.md) and update its provenance table (date,
|
||||
commit, path/operation counts) in the same change. A snapshot with stale provenance is what that
|
||||
convention exists to prevent.
|
||||
- **[`docs/status/backlog.md`](../../../docs/status/backlog.md)** — tick the row if this closes a filed
|
||||
item. Never delete a row; a ticked row is the record that it shipped.
|
||||
- **A reference file in `docs/rules/server/`** — only if the feature introduces a genuinely new reusable
|
||||
pattern, seam, or base class. Don't add prose for a feature that just follows the existing pattern.
|
||||
|
||||
---
|
||||
|
||||
## 7. Before you call it done
|
||||
|
||||
Run the server gate — `dotnet build Baya.sln` (**zero new warnings**) and `dotnet test Baya.sln` — and read
|
||||
your diff as if reviewing the PR. Full "what done means" checklist:
|
||||
[git-and-gates.md](../../../docs/rules/shared/git-and-gates.md) §2.
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
name: flow-testing
|
||||
description: >-
|
||||
Boot both sides of Balinyaar locally and walk a real user journey end to end — the right seeded account,
|
||||
the right flow doc, and knowing whether you just proved the real path or a mock answering. Use before
|
||||
claiming a fix or feature works, or when asked to test, verify, or demo a flow.
|
||||
---
|
||||
|
||||
# Balinyaar Flow Testing
|
||||
|
||||
Exercising a flow proves something only if you know which half of the stack actually answered. This is the
|
||||
procedure; the facts it points at (ports, accounts, known failure modes) live in
|
||||
[docs/flows/testing-setup.md](../../../docs/flows/testing-setup.md) and are kept current there — don't copy
|
||||
them here, they will drift.
|
||||
|
||||
---
|
||||
|
||||
## 1. Boot it
|
||||
|
||||
The five-minute path, verbatim from [testing-setup.md](../../../docs/flows/testing-setup.md#the-five-minute-path):
|
||||
|
||||
```bash
|
||||
# API — mock SMS or request_otp 500s
|
||||
cd server
|
||||
Seams__Sms__Provider=mock dotnet run --project src/API/Baya.Web.Api/Baya.Web.Api.csproj
|
||||
|
||||
# client
|
||||
cd client && npm install && npm run dev # http://localhost:3000/fa
|
||||
|
||||
# read the OTP — the console does NOT print it
|
||||
curl http://localhost:5002/api/v1/dev/last_otp/09120000010
|
||||
```
|
||||
|
||||
No database setup: the committed dev config points at an already-seeded remote SQL Server. If anything here
|
||||
doesn't match reality when you run it, **testing-setup.md is wrong and needs a fix in the same change** — it
|
||||
carries a `Last verified` stamp for exactly this reason.
|
||||
|
||||
---
|
||||
|
||||
## 2. Check the mock-vs-real map *before* you conclude anything
|
||||
|
||||
A flow "working" through a mocked domain proves the UI, not the server. Before testing:
|
||||
|
||||
1. Open [docs/integration/domains/index.md](../../../docs/integration/domains/index.md) — the census table
|
||||
names which of the 22 client `services/` domains are real vs **mock** (currently 15 real, 7 mock:
|
||||
`admin`, `bnpl`, `partnerCenter`, `patientRecords`, `payouts`, `refunds`, `verification`).
|
||||
2. A mocked domain is a `USE_<DOMAIN>_MOCK` flag in `client/src/services/<domain>/constants.ts` — check it
|
||||
directly if you need certainty for the exact domain you're touching.
|
||||
3. State your finding in terms of which one you exercised: "the booking flow works end-to-end against the
|
||||
real server" is a different claim from "the admin console renders correctly against its mock" — never
|
||||
report the second as if it were the first.
|
||||
|
||||
---
|
||||
|
||||
## 3. Pick the right seeded account
|
||||
|
||||
Demo accounts, their roles, and what each one demonstrates are tabulated in
|
||||
[testing-setup.md](../../../docs/flows/testing-setup.md#demo-accounts) — read it there rather than assuming
|
||||
a phone number. One standing gap to route around: **the seeded admin accounts (`…020` `super_admin`,
|
||||
`…021` `finance`) get 403 on every real admin endpoint** (a `DynamicPermission` / role-literal mismatch).
|
||||
The admin backoffice is only testable against the client's mock; don't spend time trying to walk it against
|
||||
the real API without first checking whether that gap has been closed.
|
||||
|
||||
---
|
||||
|
||||
## 4. Walk the flow
|
||||
|
||||
[docs/flows/index.md](../../../docs/flows/index.md) is the atlas — one file per user-meaningful journey,
|
||||
each answering exactly three questions: what it does, what's mocked *for that journey specifically*, and how
|
||||
to test it. Open the one file that matches what you're testing rather than guessing the steps; it's the
|
||||
one place gap numbers and REQ references for that journey are tracked.
|
||||
|
||||
---
|
||||
|
||||
## 5. Two things that will silently invalidate your test
|
||||
|
||||
- **The scheduler is live while you test.** `booking_request_expiry` runs every 60 seconds (hardcoded) and
|
||||
flips an un-actioned request to `expired_no_response` / `payment_deadline_expired` out from under you. Act
|
||||
on a request promptly, or create a fresh one rather than trying to reuse an old test artifact.
|
||||
- **The OTP endpoints are rate-limited together.** `request_otp` and `verify_otp` share one bucket, 5 calls
|
||||
per 60 s per IP — a login is 2 calls, so that's **two logins per minute, total**. Space scripted logins
|
||||
≥ 40 s apart (see [testing-setup.md](../../../docs/flows/testing-setup.md#scripting-logins) for a working
|
||||
script) or you'll 429 and misread it as a bug.
|
||||
|
||||
---
|
||||
|
||||
## 6. When the seeded world has aged out
|
||||
|
||||
There is no in-app reseed — both seeders guard on natural keys, so re-running never refreshes stale dates.
|
||||
If the scenario you need (an "upcoming" booking, an open dispute window, a pending request) no longer exists
|
||||
because the world was seeded days ago:
|
||||
|
||||
- **Fastest fix:** create the scenario fresh yourself (customer → search → booking request → accept → pay) —
|
||||
this is the intended way to exercise booking-request and checkout-and-payment anyway.
|
||||
- **Full reseed:** only against a **local** database — `docker compose down -v && docker compose up -d` under
|
||||
`server/`, then boot. **Never drop the shared remote database** casually; it backs the live demo deployment
|
||||
and other people's sessions.
|
||||
|
||||
---
|
||||
|
||||
## 7. Report what you actually saw
|
||||
|
||||
Name the account you used, the domain's mock/real status, and the exact response (status code, error
|
||||
message) rather than "it worked" — the troubleshooting table in
|
||||
[testing-setup.md](../../../docs/flows/testing-setup.md#troubleshooting) exists because several failure
|
||||
modes here look identical to an unrelated bug (a rate limit looks like a crash; `/healthz/ready` failing on
|
||||
Windows looks like the app is down). Check it before filing something as a new defect.
|
||||
Reference in New Issue
Block a user