Files
baya-monorepo/dev/shared-working-context/backend/handoff/after-backend-phase-5.md
T
hamid f77a23cb25 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>
2026-07-02 22:21:53 +03:30

6.4 KiB
Raw Blame History

After backend-phase-5 — the service catalog & nurse pricing variants are live

The two-tier service model the whole marketplace is priced and searched on now exists. Admins curate a catalog skeleton (categories → configurable option groups → option values) that ships as data, not migrations, and each nurse turns that skeleton into variants — the atomic bookable unit: a category + one chosen value per dimension, at the nurse's own price and price unit. Contract: dev/contracts/domains/catalog.md; machine schema: dev/contracts/openapi/swagger.v1.json (refreshed for b5).

What the frontend (f4-b5) can now build

  • Category grid / browseGET api/v1/catalog/categories (active, ordered, paginated, cached). Five categories are seeded (Elderly, Post-Surgery, Infant, Chronic, Companionship) with nameFa+nameEn.
  • The nurse service-builder — for a chosen category, GET api/v1/catalog/option_groups?category_id= returns the applicable dimensions (the category's own groups plus every cross-category group), each with isRequired and its active values. The nurse then POST api/v1/nurse_variants/create { serviceCategoryId, options: [{ optionGroupId, optionValueId }], price, priceUnit, sessionCount?, displayName? }. Manage with update/{id}, set_active/{id}, list, get/{id}. Requires a nurse profile first (b3 nurse_profiles/upsert).
  • Admin catalog consoleadmin_catalog/create_category|update_category/{id}|set_category_active/{id}, create_option_group|update_option_group/{id}, create_option_value|update_option_value/{id} (admin token / dynamic-permission).
  • Public variant viewGET api/v1/nurse_variants/get/{id} returns the public (active-only) projection for anyone, backing a nurse-profile offerings list.

Rules baked into the API (don't fight them client-side)

  • The bookable unit is the variant, not the nurse. Price/book against a specific variant.
  • price is a string of IRR-Rial digits (e.g. "8000000") — integer money, no floats, no Toman. The total is price + priceUnit + sessionCount; never compute it from price alone. Parse with a BigInt-safe helper, format for display, map priceUnit to an i18n label (never off the code).
  • A NULL-category option group is cross-category — it applies to every category. Render the cross- category groups in the builder for every category; the required ones must be answered.
  • All required dimensions must be answered, one value per dimension, a value must belong to its group. A missing required dimension → 400 (names it); a duplicate identical listing → 409; surface both cleanly.
  • displayName auto-generates (category + chosen value labels) — show it, let the nurse override it.
  • Deactivate, never delete. A deactivated variant stays in the nurse's own list (flagged isActive:false) but is unbookable and 404s on the public get.
  • Every catalog row has nameFa (primary) + nameEn — pick by locale, never label off a code.
  • Tenancy — variants are strictly owner-scoped; another nurse's id on write → 404 (existence not leaked). Catalog skeleton writes are admin-only.
  • Refresh after select_role still applies (nurse scoping reads the role claim in the token).
  • Routes are action-style (admin_catalog/create_category, nurse_variants/create, …), POST for mutations, camelCase bodies — see the contract for the full list.

What b7 (search & matching) must read from the variant

The variant is a clean, projection-friendly source. b7 owns the denormalized nurse_search_index and the INurseSearch seam (not built here). Its fan-out reads, per active variant: service_category_id, price, price_unit, is_active, nurse_id — joined to the nurse's nurse_service_areas (b4) to emit one index row per covered city/district, and to nurse_profiles for is_verified/accepting/gender/rating. The single write trigger points to maintain that index are CreateVariantCommand, SetVariantActiveCommand (and later option/price edits) — hook the index maintenance there.

What b8 (booking) consumes

IVariantSnapshotSerializer (Application/Contracts/Common, single real impl in Application/Common) is ready: string Serialize(VariantSnapshot) emits the canonical variant_snapshot_json (category id + labels, each (group label, value label), price as a digit string, price_unit, session_count, display_name, variant_id). b8 owns the booking_requests.variant_snapshot_json column and calls the serializer at booking time so later variant edits/deactivation never mutate past bookings. The snapshot column is not added here.

Schema / migration

Migration 20260702132758_ServiceCatalogAndNurseVariants (applies on startup), new catalog schema: ServiceCategories, ServiceOptionGroups (nullable ServiceCategoryId = cross-category), ServiceOptionValues, NurseServiceVariants (Price BIGINT, PriceUnit code, SessionCount?, DisplayName, OptionSetHash), NurseServiceVariantOptions. Constraints: UNIQUE(VariantId, OptionGroupId) (one value per dimension); filtered UNIQUE(NurseId, ServiceCategoryId, OptionSetHash) WHERE DeletedAt IS NULL (duplicate-listing backstop); soft-delete query filters. Seed: five categories (nameFa+nameEn), ids 15. Option groups/values are admin-authored data (not seeded).

Deferred to later phases (do not build against these yet)

  • nurse_search_index, INurseSearch, the search query & index fan-outb7 (variant writes are the clean trigger point; the variant shape is projection-ready).
  • variant_snapshot_json persistenceb8 (the serializer is shipped and unit-tested here).
  • nurse_availability_slots / nurse_availability_exceptionsdeferred (soft scheduling guidance, not on the money/safety path) — not built.
  • Holiday/surge pricing, a Companionship tier pricing model, tiered per-category commissiondeferred. Companionship ships only as a seeded category (data), not a special pricing path.

What's mocked

Nothing. Catalog and variant data are fully owned by Balinyaar's DB — this phase introduces no cross-cutting seam and adds no row to reports/mocks-registry.md. It reuses ICacheService (b0) for the public catalog reads with invalidate-on-mutation.