backend phase 5: service catalog & nurse pricing variants

Two-tier service model the marketplace is priced and searched on. Admin
catalog skeleton (categories + EAV option groups/values, addable as data
not migrations; NULL category = cross-category) and the nurse pricing layer
(nurse_service_variants — the atomic bookable unit: category + one value per
required dimension at the nurse's own IRR price and price unit).

- New `catalog` schema via one additive migration; Price BIGINT (no floats),
  on the wire as a string of digits; total = price + unit + session_count.
- Duplicate-listing guard: deterministic option_set_hash + filtered
  UNIQUE(nurse_id, service_category_id, option_set_hash) WHERE deleted_at IS
  NULL + friendly 409 pre-check. One value per dimension; required groups
  (incl. cross-category) enforced; deactivate, never delete.
- Public catalog browse cached behind a CatalogCache generation token,
  invalidated on any admin write. IVariantSnapshotSerializer shipped for b8.
- Contract (catalog.md) + handoff + report published; swagger refreshed.
  122 tests green; zero new build warnings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamid
2026-07-02 22:21:53 +03:30
parent 4b4243c451
commit f77a23cb25
84 changed files with 8229 additions and 4 deletions
@@ -12,6 +12,31 @@ One block per completed backend phase. Newest at the top. Backend lane writes he
- **Notes for frontend:** <anything load-bearing>
-->
## backend-phase-5 — Service catalog & nurse pricing variants — 2026-07-02
- **Shipped:** five tables via one additive migration (`ServiceCatalogAndNurseVariants`) — new **`catalog`**
schema `ServiceCategories` / `ServiceOptionGroups` (nullable `service_category_id` = cross-category) /
`ServiceOptionValues` / `NurseServiceVariants` (`Price` **BIGINT IRR**, `PriceUnit`, `SessionCount?`,
`DisplayName`, `OptionSetHash`) / `NurseServiceVariantOptions` (`UNIQUE(variant_id, option_group_id)`);
seed of **5 categories** (`nameFa`+`nameEn`) via `HasData`. 16 CQRS slices across 3 controllers
(`catalog` public browse, `admin_catalog` CRUD + set-active, `nurse_variants` create/update/set-active/
list/get). Duplicate-listing guard = `OptionSetHash` + filtered `UNIQUE(nurse_id, service_category_id,
option_set_hash) WHERE deleted_at IS NULL` + 409 pre-check. Public catalog reads cached behind a
`CatalogCache` generation token (invalidate on any admin write). Ships **`IVariantSnapshotSerializer`**
(pure, for b8). **No new seam.**
- **Contracts:** dev/contracts/domains/catalog.md + openapi snapshot refreshed (yes — 14 new
catalog/admin_catalog/nurse_variants paths; 71 total).
- **Mocked:** none — this phase mocks nothing and adds **no** `reports/mocks-registry.md` row.
- **Gate:** build clean (0 new code warnings) / tests green (122 pass: +10 handler/serializer unit,
+11 `Baya.Test.Api` integration). Migration verified to apply on a real SQL Server; swagger exposes all
b5 paths. Adversarial 4-dimension review: 0 confirmed findings.
- **Handoff:** backend/handoff/after-backend-phase-5.md
- **Notes for frontend:** the **variant is the bookable unit** (not the nurse). `price` is a **string of IRR
digits**; the total is `price` + `priceUnit` + `sessionCount`, never price alone. A **NULL-category option
group is cross-category** (render it under every category; required ones must be answered). Duplicate
identical listing → **409**; missing required dimension → **400**. `displayName` auto-generates (editable).
Deactivate, never delete. Routes are action-style POST (`admin_catalog/create_category`,
`nurse_variants/create`, …); groups/values are admin-authored (only categories are seeded).
## backend-phase-4 — Geography, addresses & nurse service areas — 2026-07-02
- **Shipped:** five tables via one migration (`GeographyAddressesServiceAreas`) — new **`geo`** schema
`Provinces` 1:N `Cities` 1:N `Districts` (+ `NurseServiceAreas`) and `usr.CustomerAddresses`; seed