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:
@@ -300,6 +300,18 @@ Wire `ICurrentUser` (HTTP context accessor wrapped in an interface, registered S
|
||||
|
||||
Every monetary value is **IRR Rials stored as `long` / `BIGINT`**. There is **no float/decimal path** on money — not in entities, DTOs, the API, or arithmetic. Toman is display-only and converts to/from Rials **only** inside a provider adapter at its boundary, never in domain or shared code. If a money value object is introduced later it must be integer-only. The three booking amounts always satisfy `gross = commission + payout`.
|
||||
|
||||
### Deterministic set-hash for multi-row uniqueness
|
||||
|
||||
When "no two rows may share the same *set* of child rows" must be enforced (e.g. a nurse can't list two
|
||||
identical variants — same category + identical answered option-set), a plain composite unique index can't
|
||||
express it because the set spans multiple rows. Reduce the set to a single comparable column with
|
||||
**`Baya.Application.Common.OptionSetHash.Compute(pairs)`** (backend-phase-5): it sorts the `(long, long)`
|
||||
pairs and SHA-256s them to a stable 64-char hex hash that is **order-independent** (identical sets always
|
||||
collide). Persist it (`NVARCHAR(64)`) and back it with a **filtered unique index** (e.g.
|
||||
`UNIQUE(nurse_id, service_category_id, option_set_hash) WHERE deleted_at IS NULL`) as the race-safe backstop,
|
||||
with a handler pre-check for the friendly `409`. Reuse this helper for any future "same set of ids" guard;
|
||||
do **not** reuse `IFieldEncryptor.Hash` (that is for PII-column equality lookups).
|
||||
|
||||
---
|
||||
|
||||
## 7. Validation
|
||||
|
||||
Reference in New Issue
Block a user