Skip to main content

EP-016 — Spa & Wellness Management RTM

EPIC: EP-016 Spa & Wellness Management Reconciled: 2026-05-25 Wave 1 — parallel per-EPIC RTM scale-out (BF-011 Spa Booking is the eventual flow doc — order 11 in PLAYBOOK.md). Source of truth for code state: hmp-spa-service/CLAUDE.md + live src/ tree; review files under reviews/EP-016/ are baseline as of 2026-03-08 and stale on AC-by-AC verdicts where billing has hardened (see D-006). Catalog vs EPIC body: numbering drift on stories 4-9 (D-001 below). This RTM walks the EPIC body story IDs/titles since both epics/EP-016-spa-wellness.md (live) and the reviews/EP-016/US-016-NNN-review.md files describe nine stories that match the EPIC body 1:1 — but the catalog (01-epic-catalog.md §EP-016) ships a different nine-story list. Both readers will land on this RTM; D-001 reconciles.

Status legend​

StatusMeaning
BuiltCode exists + tests exist + tests pass
Built-UntestedCode exists, no automated test coverage (or test exists but is a stub)
PartialCode exists for some acceptance criteria, gaps for others
StubCode is a placeholder / TODO / returns mock data
Not StartedNo code

Summary​

MetricCount
Total stories (EPIC body)9
EPIC-body claims "Built"3 (US-016-001, -002, -003)
EPIC-body claims "Partial"2 (US-016-004 Calendar, US-016-005 Folio)
EPIC-body claims "Not Started"4 (US-016-006, -007, -008, -009)
Verified-Built1 (US-016-003 — covered by 16-test scheduling.spec + 10-test cross-tenant + spa-booking.flow)
Built-Untested1 (US-016-005 — billing.service.ts hardened well beyond review verdict, but no end-to-end frontdesk→folio assertion in flow tests)
Partial4 (US-016-001 CRUD-only / no search-image-role-guard; US-016-002 no leave / no deactivation guard; US-016-004 list view only no calendar grid; US-016-009 nothing in spa schema for health forms)
Stub0
Not Started3 (US-016-006 Prepaid Sessions; US-016-007 Couple/Group; US-016-008 Reports)
Drift findings11 (D-001..D-011; 4 P0 / 5 P1 / 2 P2)
[SME-PROPOSED] refinements18

US-016-001 — Treatment Catalog Management​

EPIC-body status: Built (P0, 5 points) Catalog status: Built (P1, 3 points) — title catalog-side is "Treatment Catalog CRUD (Services + Packages)" which conflates US-016-001 with the package model (see D-001). Verified status: Partial — core CRUD shipped + tested, but every spec'd extension (search, unique-name 409, image upload, dedicated PATCH /status, role guard, HSN field) is missing. Review file US-016-001-review.md lists 9 gaps with severity HIGH on duplicate-name + role-guard + image upload.

Code locations:

  • hmp-spa-service/src/treatments/treatments.service.ts — Treatment CRUD; soft-delete via isActive=false
  • hmp-spa-service/src/treatments/treatments.controller.ts — GET /spa/treatments, POST /spa/treatments, GET /spa/treatments/:id, PATCH /spa/treatments/:id, DELETE /spa/treatments/:id
  • hmp-spa-service/src/treatments/dto/ — CreateTreatmentDto, UpdateTreatmentDto, TreatmentFilterDto
  • hmp-web-dashboard/src/app/(dashboard)/spa/treatments/page.tsx — Treatments page (static placeholder per review)
  • hmp-booking-engine/src/components/spa/SpaTreatmentBrowser.tsx — Guest-side treatment browser
  • hmp-booking-engine/src/lib/api/spaApi.ts + mockSpaApi.ts — API client (mock fallback in dev)

Unit + integration tests:

  • hmp-spa-service/src/treatments/treatments.service.spec.ts — 8 tests covering findAll (with filters), findOne, create, update, soft-delete. No tests for search / duplicate-name conflict / image upload.

Flow tests:

  • hmp-infrastructure/testing/flow-tests/src/flows/spa-booking.flow.spec.ts :: "Scenario 1 — a spa treatment exists (list or create)" — TBD (not enumerated in 13-flow-demo-readiness doc, but the spec is well-formed and is order-11 BF-011 per PLAYBOOK.md).

Real-backend smokes:

  • hmp-web-dashboard/e2e/real-backend/smoke-spa.real.spec.ts — covers /spa page loading with seeded treatments
  • hmp-web-dashboard/e2e/tests/spa-management.spec.ts — spa management Playwright suite

Use cases referencing this story: none found in _data/use-cases/ for spa treatments specifically.

SME notes:

  • SpaTreatment.taxRate defaults to 18 (GST 18% on spa services per Indian SAC 999722) per prisma/schema.prisma — the right India default, but the story body does not enumerate the SAC/HSN code on output payloads. Review GAP-1 ("No text search") and GAP-4 ("No image upload endpoint") are the most operationally visible — staff browsing 50+ treatments cannot filter by keyword today.
  • The catalog conflates Treatment Catalog (this story) with Packages (own story US-016-006 in the EPIC body — Prepaid Package & Session Tracking). Code splits these into separate modules (treatments/ and packages/) which matches the EPIC body, not the catalog title.
  • No role guard beyond JwtAuthGuard (GAP-6, severity HIGH). Any authenticated user across tenants can modify the treatment catalog — high-risk surface for an OTA or franchise multi-tenant setup.

Drift findings:

D-001 — [P0] [Catalog-vs-EPIC-body] Catalog (01-epic-catalog.md §EP-016) and EPIC body (epics/EP-016-spa-wellness.md) disagree on which title goes with which story ID from US-016-004 onward:

  • Catalog US-016-004 = "Spa Charge Posting" vs EPIC US-016-004 = "Spa Calendar View"
  • Catalog US-016-005 = "Spa Calendar View" vs EPIC US-016-005 = "Charge Posting"
  • Catalog US-016-006 = "Spa Revenue Reports" vs EPIC US-016-006 = "Prepaid Package & Session Tracking"
  • Catalog US-016-007 = "Package/Membership Tracking" vs EPIC US-016-007 = "Couple & Group"
  • Catalog US-016-008 = "Couple/Group Appointment Booking" vs EPIC US-016-008 = "Spa Revenue Reports"
  • Catalog US-016-009 = "Guest Spa Check-In" vs EPIC US-016-009 = "Guest Health & Consent Form"

Same problem-shape as EP-002 D-007 (BF-001 pilot). The reviews/EP-016/US-016-NNN-review.md files (only -001 through -004 exist) follow the EPIC-body numbering. Three review files reference the EPIC-body title — US-016-004-review.md is for "Spa Charge Posting to Guest Folio" matching EPIC US-016-005, which is a separate review-vs-EPIC drift.

  • [SME-PROPOSED] Adopt the EPIC-body numbering as canonical (the code modules appointments/, billing/, packages/ map cleanly to EPIC ordering). Rewrite the catalog §EP-016 table to match. Add alias notes to each review file documenting the old catalog ID. Apply the same fix-shape as EP-002 reconciliation.

D-002 — [P1] [Story-incomplete + India-compliance-critical] Treatment story does not enumerate HSN/SAC code field as a required attribute. SAC 999722 (Beauty & Physical Well-Being Services) carries 18% GST and is the canonical SAC for spa services in India. Without HSN/SAC on the treatment row, downstream folio posting cannot supply the correct SAC, breaking e-invoice IRN generation (NIC schema 1.1 requires per-line HSN/SAC for B2B).

  • [SME-PROPOSED] Add AC: "SpaTreatment SHALL carry a required sacCode field (4-8 digit numeric, default 999722). All catalog browse responses (staff + guest-facing) SHALL include sacCode. Folio posting via SPA_CHARGE_CREATED SHALL forward sacCode in payload (see D-008)."
  • [SME-PROPOSED] Add AC: "Treatment catalog SHALL enforce @@unique([propertyId, name]) (closes review GAP-2). Duplicate creates return 409 code=SPA_TREATMENT_NAME_DUPLICATE."

US-016-002 — Therapist Profile & Availability Management​

EPIC-body status: Built (P0, 5 points) Catalog status: Built (P1, 3 points) — title "Therapist Management (Availability, Specializations)" matches EPIC. Verified status: Partial — CRUD + availability endpoint shipped + JSON schema-validated (Gotcha P1-B4); leave management, deactivation guard, email uniqueness, and role guard NOT implemented (review file lists 12 gaps incl. 4 HIGH severity).

Code locations:

  • hmp-spa-service/src/therapists/therapists.service.ts — Therapist CRUD + getAvailability() returning 15-min interval slots
  • hmp-spa-service/src/therapists/therapists.controller.ts — CRUD + GET /spa/therapists/:id/availability
  • hmp-spa-service/src/therapists/availability.validator.ts — Custom @IsTherapistAvailability decorator (P1-B4) — accepts {monday..sunday: {start:'HH:MM', end:'HH:MM'} | null, exceptions: [{date:'YYYY-MM-DD', closed?, start?, end?}]}; rejects unknown keys, malformed times, start>=end, split-shift arrays.
  • hmp-spa-service/src/therapists/dto/ — CreateTherapistDto, UpdateTherapistDto, TherapistFilterDto
  • hmp-web-dashboard/src/app/(dashboard)/spa/therapists/page.tsx — Therapists page (static card list per review)

Unit + integration tests:

  • [CITED-BUT-NOT-FOUND] — therapists.service.spec.ts does NOT exist. Review file confirms: "No therapists.service.spec.ts file exists". Availability validator has no co-located spec either.

Flow tests: none specific to therapists.

Real-backend smokes: covered indirectly via smoke-spa.real.spec.ts (page loads with seeded therapists).

Use cases referencing this story: none.

SME notes:

  • The availability JSON schema validator (availability.validator.ts, P1-B4) is the cleanest piece of code in the module — typed shape, exception calendar, defensive against split-shifts the scheduler can't handle. That's a real win, but no spec asserts it.
  • Indian-spa context: most resort therapists work split shifts (morning + evening, gap mid-day). The current validator REJECTS split shifts because scheduling.service.ts only handles a single window/day. The story body does not flag this constraint — operators in Kerala Ayurveda resorts will hit it on day-1.
  • No leave management, no deactivation guard against future appointments, no email uniqueness — all flagged HIGH-severity in review.
  • "Lakshmi Nair" example in the EPIC body AC is culturally appropriate (Kerala Ayurveda) — but the underlying data model has no homeLanguage or gender-preference-acceptance field. Female guests often request female therapists for body treatments (Indian cultural norm); the booking flow has no way to honor or even surface this.

Drift findings:

D-003 — [P0] [Test-vs-code] Zero unit tests for TherapistsService despite Built status in EPIC body. Therapist availability is the most complex JSON-shape in the spa schema and the only spa write-path with a custom class-validator decorator — totally untested.

  • [SME-PROPOSED] Author hmp-spa-service/src/therapists/therapists.service.spec.ts with min coverage: CRUD happy path, availability JSON validation (reject split-shift, reject start>=end, reject unknown keys, accept exception calendar with closed dates), specialization filter via Prisma has, soft-deactivate, multi-tenant isolation.
  • [SME-PROPOSED] Add AC: "Deactivating a therapist with active future appointments (status BOOKED or CONFIRMED, startTime > now) SHALL return 422 code=THERAPIST_HAS_FUTURE_APPOINTMENTS with the count + first 5 appointment IDs in payload. Operator must reassign or cancel before deactivating." Closes review GAP-3.

D-004 — [P1] [Story-incomplete + India-cultural-context] No gender-pairing preference on appointment OR therapist. Indian spa context: female guests routinely request female therapists for body massage/Ayurveda. Today the system has no way to honor this — receptionists work around by maintaining a paper list.

  • [SME-PROPOSED] Add AC: "SpaAppointment SHALL accept an optional requestedTherapistGender enum (MALE | FEMALE | NO_PREFERENCE, default NO_PREFERENCE). When set, scheduling auto-assignment SHALL match only therapists whose gender field equals the request. Booking engine SHALL expose this on the guest-facing form."

US-016-003 — Spa Appointment Booking​

EPIC-body status: Built (P0, 8 points) Catalog status: Built (P1, 5 points) Verified status: Built — scheduling + status machine + cross-tenant denial + flow test all shipped. Auto-assignment, operating hours check, leave check, specialization matching missing (review file GAPs 1-4, all HIGH).

Code locations:

  • hmp-spa-service/src/appointments/appointments.service.ts — 348 LOC; create(), findOne(tenantId, id), findAll(tenantId, filter, pagination), updateStatus(tenantId, id, dto). Status machine enforced via STATUS_TRANSITIONS map; Kafka events on each transition. Cross-tenant guard: findUnique + post-check throwing ForbiddenException on tenant mismatch (information-leak; see D-005).
  • hmp-spa-service/src/appointments/appointments.controller.ts — GET /spa/appointments, GET /spa/appointments/availability, POST /spa/appointments, GET /spa/appointments/:id, PATCH /spa/appointments/:id/status, POST /spa/appointments/:id/folio. @Roles('SUPER_ADMIN','ADMIN','MANAGER','SPA_STAFF','FRONT_DESK_AGENT') decorator on controller class.
  • hmp-spa-service/src/scheduling/scheduling.service.ts — 210 LOC; checkConflict() with 15-min buffer; getAvailableSlots() returning 15-min interval slots with booked filtering, off-days, default hours.
  • hmp-spa-service/src/appointments/dto/appointment.dto.ts — CreateAppointmentDto, UpdateAppointmentStatusDto, AppointmentFilterDto
  • hmp-web-dashboard/src/app/(dashboard)/spa/appointments/page.tsx — Appointments page (mock data per review)
  • hmp-booking-engine/src/components/spa/SpaBookingForm.tsx + SpaAvailabilityCalendar.tsx + SpaConfirmation.tsx — Guest-side booking flow

Unit + integration tests:

  • hmp-spa-service/src/scheduling/scheduling.service.spec.ts — 16 tests (8 for getAvailableSlots, 8 for checkConflict). Excellent coverage of scheduling logic.
  • hmp-spa-service/src/appointments/__tests__/appointment-cross-tenant-denial.spec.ts — 10 tests (Gold-Standard Pick D, Phase 3 rollout 2026-05-24). Covers findOne ownership (ForbiddenException leak documented), findAll tenant-scoping with forged propertyId, create-time treatment + therapist parent-tenant rejection, updateStatus ownership, missing propertyId BadRequest. This is the "orphaned-test" referenced in the task brief — it asserts current shipped behavior including the deviation from canonical pattern (NotFound vs Forbidden).
  • [CITED-BUT-NOT-FOUND] — appointments.service.spec.ts does NOT exist (review file confirms). The cross-tenant-denial spec partially covers it but not the happy path / status transition matrix.

Flow tests:

  • hmp-infrastructure/testing/flow-tests/src/flows/spa-booking.flow.spec.ts :: "Scenario 1-4 — treatment exists, book appointment, status machine BOOKED → CONFIRMED → IN_PROGRESS → COMPLETED, folio guard rejects without reservation" — TBD pass-count (not in 13-flow doc; BF-011 candidate per PLAYBOOK.md row 11).

Real-backend smokes:

  • hmp-web-dashboard/e2e/tests/spa-booking.spec.ts — Playwright suite

Use cases referencing this story: none found.

SME notes:

  • The Gold-Standard cross-tenant-denial spec is the cleanest tenant-isolation coverage in the spa module — explicitly asserts that spaTreatment.findFirst and spaTherapist.findFirst filter by tenantId (not just id). This guards against the SQL-injection-equivalent at the ORM layer (forged ID → fetch row across tenants).
  • The orphaned-test status (per task brief) is real: the spec sits in appointments/__tests__/ rather than co-located *.service.spec.ts because the canonical appointments.service.spec.ts doesn't exist. It's a defense-only spec, not part of an organic test suite.
  • Spec header on the cross-tenant spec explicitly documents the information-leak deviation (point 2): findOne throws ForbiddenException not NotFoundException, leaking row-existence across tenants. The spec asserts current behavior AND flags the TODO. This is the security gap surfaced by the task brief (Task #11).
  • Auto-assignment (GAP-1 HIGH), operating-hours check (GAP-2 HIGH), therapist-leave check (GAP-3 HIGH), specialization matching (GAP-4 HIGH) — all in review file but no remediation shipped. A booking can be made for an ENT specialist on a body massage today.

Drift findings:

D-005 — [P0] [Security + tests-assert-deviation] AppointmentsService.findOne uses prisma.spaAppointment.findUnique({where:{id}}) + post-fetch tenantId !== tenantId check throwing ForbiddenException. This leaks row existence across tenants (tenant A can probe by ID and distinguish "appointment exists in another tenant" vs "appointment does not exist"). The orphaned cross-tenant-denial spec (lines 108-123) asserts this AS the current behavior — i.e. the test pins the security gap instead of guarding against it. Canonical pattern (per hmp-pos-service/src/orders/__tests__/cross-tenant-denial.spec.ts) uses findFirst({where:{id,tenantId}}) + NotFoundException. Same vulnerability shape exists on updateStatus (lines 277-281).

  • [SME-PROPOSED] Refactor AppointmentsService.findOne and updateStatus to use findFirst({where:{id, tenantId}}) returning null → NotFoundException. Drop the ForbiddenException post-check. Update the orphaned spec lines 108-123 + 272-288 to assert NotFoundException + remove the deviation header (point 2). Same review GAP applies to BillingService.postToFolio (hmp-spa-service/src/billing/billing.service.ts:22-33) which uses the same leaky pattern.
  • [SME-PROPOSED] Add AC: "All Spa* read paths (findOne, updateStatus, postToFolio) SHALL throw NotFoundException when the row does not exist OR belongs to a different tenant. No path SHALL distinguish the two cases in the response. Asserted by spec covering both real-row-wrong-tenant and missing-id."

D-006 — [P1] [Review-stale + test-vs-code] Review file US-016-003-review.md lists 16 gaps but several were closed after review date (2026-03-08): BillingService.postToFolio IS now wired (review GAP-14 said "stub"; code at billing.service.ts:21-91 is fully implemented with status check + duplicate prevention + CGST/SGST split + Kafka publish); appointment dto includes dateFrom/dateTo (review GAP-12) per controller signature. Reading the review file alone in 2026-05 underestimates progress.

  • [SME-PROPOSED] Re-run US-016-003 review against current appointments/ + billing/ + scheduling/ modules to refresh gap list. Expected new verdict: PARTIAL (not "needs auto-assignment + ops-hours-check + leave-check + specialization-match"). Bring the review file in line with the post-2026-03-08 code state.

US-016-004 — Spa Calendar View (Day/Week)​

Note: This is the EPIC-body title. The catalog calls this story "Spa Charge Posting" (P1/3 points/Partial) — see D-001.

EPIC-body status: Partial (P1, 5 points) Catalog status: (catalog ID mismatch — see D-001) Verified status: Partial — appointments list page exists with status filter / search / table; interactive calendar grid (therapist rows × time columns), drag-and-drop reschedule, and color-coding all NOT built.

Code locations:

  • hmp-web-dashboard/src/app/(dashboard)/spa/appointments/page.tsx — Appointments page with list view
  • (backend supports the calendar — GET /spa/appointments?date=&therapistId= is sufficient; the gap is purely frontend)

Unit + integration tests: none specific to calendar (page test only — __tests__/spa-page.test.tsx).

Flow tests: none.

Real-backend smokes: indirectly via spa-management.spec.ts.

Use cases referencing this story: none.

SME notes:

  • Backend has all the data needed; the calendar gap is 100% frontend. A library like FullCalendar or dnd-kit + a Resource Timeline view would close this in 1-2 sprints — but neither is in package.json per the dashboard CLAUDE.md.
  • Color coding by treatment category aligns with SpaTreatment.category enum (MASSAGE/FACIAL/BODY_WRAP/HAIR/NAIL/AYURVEDA/WELLNESS/OTHER) — the dashboard design tokens have brand colors but no category-color mapping today.

Drift findings: (none net new — captured in summary as Partial)

  • [SME-PROPOSED] Add AC: "Spa Calendar SHALL render therapist rows × 30-min-interval columns from propertySettings.spaOpeningHours (default 09:00-20:00 IST). Each appointment block renders with data-treatment-category attribute → CSS class mapping → category colors (Ayurveda=#2E7D32 green, Massage=#1976D2 blue, Facial=#E91E63 pink, others=neutral)."

US-016-005 — Charge Posting to Guest Folio​

Note: This is the EPIC-body title. The catalog calls this story "Spa Calendar View" (P2/3 points/Built) — see D-001.

EPIC-body status: Partial (P0, 5 points) Catalog status: (catalog ID mismatch — see D-001) Verified status: Built-Untested — BillingService.postToFolio is fully wired (controller endpoint, status check, duplicate prevention, CGST/SGST split, Kafka publish, paidAmount update). Receiver SpaFolioChargeConsumer exists in hmp-frontdesk-service. But: NO end-to-end flow-test assertion that the folio row appears, NO reversal endpoint, NO direct-payment (walk-in) path, NO multi-folio (split-billing) support, NO HSN/SAC in payload, NO inter-state IGST resolution.

Code locations:

  • hmp-spa-service/src/billing/billing.service.ts — 92 LOC; postToFolio(tenantId, appointmentId) validates appointment exists + tenant matches + status==='COMPLETED' + paidAmount===0 + reservationId set; computes CGST/SGST (50/50 split); publishes SPA_CHARGE_CREATED; updates paidAmount=totalAmount.
  • hmp-spa-service/src/appointments/appointments.controller.ts:127-137 — POST /spa/appointments/:id/folio route delegates to billingService.postToFolio() (no longer the stub the review file said it was).
  • hmp-frontdesk-service/src/folios/consumers/spa-folio-charge.consumer.ts — Receiver creating the FolioTransaction row.

Unit + integration tests:

  • hmp-spa-service/src/billing/billing.service.spec.ts — 6 tests covering Kafka event, paidAmount update, appointment not found, no reservation (walk-in guard), tax calc, description.
  • hmp-frontdesk-service/src/folios/consumers/spa-folio-charge.consumer.spec.ts — Receiver spec
  • hmp-frontdesk-service/src/folios/consumers/folio-charge-posting.consumer.spec.ts — Generic charge consumer

Flow tests:

  • hmp-infrastructure/testing/flow-tests/src/flows/spa-booking.flow.spec.ts :: "Scenario 4 — post charges to folio — rejects without reservation linkage (guard)" — TBD. Note: scenario only asserts the GUARD (400 on missing reservationId) — does NOT assert end-to-end that a folio row is created when reservationId IS present. The cross-EPIC handoff is untested.

Real-backend smokes: none specific.

Use cases referencing this story: none found.

SME notes:

  • Billing IS substantially better than the review file suggests — the stale-review problem is acute here. Review file says "Controller's POST :id/folio route returns 'not yet implemented' stub" (GAP-14 HIGH). Current code at line 127-137 of appointments.controller.ts delegates properly. The review predates the closure. Same trap as D-006.
  • CGST/SGST is always 50/50 split in the current code (taxAmount / 2). This is correct for intra-state sales (property state == guest billing state). For inter-state (guest from Karnataka staying at a Goa property), India levies IGST instead of CGST+SGST. The current code does not distinguish — guest state isn't even read. This silently mis-classifies inter-state spa charges and breaks GST return filing (GSTR-1 has separate columns for CGST/SGST vs IGST).
  • HSN/SAC is NOT in the Kafka payload (review GAP-4). Without it, the frontdesk-service consumer cannot enrich the FolioTransaction for e-invoice IRN generation. NIC IRP schema 1.1 rejects line items without HSN/SAC for B2B invoices > ₹500.
  • No spa.charge.reversed event, no POST /spa/appointments/:id/folio/reverse endpoint. A spa appointment marked COMPLETED then later disputed cannot be reversed in the platform — operator must hand-edit the folio in frontdesk UI.

Drift findings:

D-007 — [P0] [Cross-service-handoff + no-end-to-end-assertion] Spa charge posting is the most complex cross-service event in the spa module (spa-service → Kafka → frontdesk-service → folio row → guest folio total update). The flow test asserts ONLY the guard path (no reservation → 400); no test path asserts the success case end-to-end. The receiver (SpaFolioChargeConsumer) is mocked separately. A regression breaking the event wire (typo in topic name, payload-shape drift, missing tenantId on the consumer side) would not fail any test.

  • [SME-PROPOSED] Add AC: "BF-011 flow test SHALL include a positive path: check-in a reservation → book spa appointment with reservationId → walk status to COMPLETED → POST /folio → poll GET /frontdesk/folios/:reservationId until the spa charge appears (≤2s p95) → assert FolioTransaction.amount === appointment.totalAmount and transactionCode === 'SPA'. Mirrors the BF-001 read-after-write convention proposed in EP-002 D-001."

D-008 — [P0] [India-compliance-critical + GST-split] CGST/SGST is hard-coded 50/50 in billing.service.ts:55-57. No IGST path. No HSN/SAC in payload. Inter-state guests (~30% of booked stays at any Indian resort) silently get mis-classified GST, breaking GSTR-1 return filing.

  • [SME-PROPOSED] Add AC: "BillingService.postToFolio SHALL resolve CGST+SGST vs IGST by joining reservation.guest.billingStateCode against property.gstStateCode. Same-state → CGST+SGST 50/50. Different-state → IGST (full taxAmount in IGST field, CGST+SGST=0). Both paths covered by spec asserting payload fields."
  • [SME-PROPOSED] Add AC: "SPA_CHARGE_CREATED payload SHALL include sacCode (from treatment.sacCode, default 999722) and igst/cgst/sgst decimal fields. Frontdesk-service SpaFolioChargeConsumer SHALL forward sacCode into FolioTransaction.hsnSacCode, enabling downstream e-invoice IRN generation per NIC schema 1.1."

D-009 — [P1] [Story-incomplete + reversal-gap] No reversal endpoint or event. Spa charges posted in error stay on the folio until manually edited frontdesk-side.

  • [SME-PROPOSED] Add AC: "POST /spa/appointments/:id/folio/reverse SHALL emit SPA_CHARGE_REVERSED (with original folioTransactionId if available) and reset appointment.paidAmount=0. Role-gated to MANAGER | ADMIN | SUPER_ADMIN. Idempotent — calling twice on the same appointment returns 409 code=SPA_CHARGE_ALREADY_REVERSED."

US-016-006 — Prepaid Package & Session Tracking​

Note: This is the EPIC-body title. The catalog calls this story "Spa Revenue Reports" (P2/3 points/Partial) — see D-001.

EPIC-body status: Not Started (P1, 8 points) Catalog status: (catalog ID mismatch — see D-001) Verified status: Not Started (on prepaid tracking) — PackagesService exists for package definitions (bundled treatments + price + validity dates) but the per-guest purchase, session counter, and expiry enforcement are entirely absent.

Code locations:

  • hmp-spa-service/src/packages/packages.service.ts — 231 LOC; CRUD on SpaPackage + SpaPackageItem definitions (catalog level — what packages exist).
  • hmp-spa-service/src/packages/packages.controller.ts — Package CRUD endpoints.
  • hmp-spa-service/src/packages/packages.service.spec.ts — Spec exists (package definition CRUD).
  • (no PackagePurchase / PackageSession / SessionUsage entities in the Prisma schema per hmp-spa-service/CLAUDE.md Models table)
  • hmp-web-dashboard/src/app/(dashboard)/spa/packages/page.tsx — Packages page (catalog browsing, no per-guest tracking)
  • hmp-booking-engine/src/components/spa/SpaPackageCard.tsx — Guest-side package card (browse only)

Unit + integration tests: packages.service.spec.ts covers definition CRUD only.

Flow tests: none.

Real-backend smokes: indirectly via spa-management.spec.ts.

Use cases referencing this story: none.

SME notes:

  • The CLAUDE.md Models table explicitly lists SpaPackage + SpaPackageItem (catalog) — no PackagePurchase / PackageRedemption / PackageSessionUsage entities. Confirms Not Started for tracking.
  • Indian-spa context: prepaid wellness packages are the highest-margin spa product (Kerala Ayurveda 14-day Panchakarma packages sell for ₹40K-₹120K). Without per-guest tracking, the property loses count of remaining sessions across stays — recipients hand-write balance on paper coupons. Resort chains specifically demand this for repeat-guest loyalty programs.
  • Validity-period enforcement (validUntil per the EPIC AC) connects to FSSAI rules around prepaid wellness deals — packages MUST disclose validity period in the sale contract; auto-expiry tracking is a consumer-protection compliance helper.

Drift findings:

D-010 — [P1] [Story-incomplete + entity-model-missing] EPIC body promises per-guest tracking but no Prisma model for PackagePurchase exists. The story is currently "Not Started" but the EPIC body lists it as PRIORITY P1 (high) with 8 points — a high-priority Not Started in a P0 module is a planning gap.

  • [SME-PROPOSED] Add AC: "Prisma SHALL define PackagePurchase { id, tenantId, propertyId, packageId, guestId, totalSessions, usedSessions, remainingSessions, validFrom, validUntil, paymentStatus, paymentReference, totalPaid, createdAt } and PackageSessionUsage { id, purchaseId, appointmentId, usedAt, sessionNumber }. Unique @@unique([purchaseId, sessionNumber])."
  • [SME-PROPOSED] Add AC: "When an appointment with packagePurchaseId linkage is marked COMPLETED, BillingService.postToFolio SHALL be SKIPPED (no folio charge), usedSessions SHALL increment by 1, and remainingSessions SHALL decrement by 1. Exhausted package (remainingSessions=0) → 409 on next booking attempt with code SPA_PACKAGE_EXHAUSTED."

US-016-007 — Couple & Group Appointment Booking​

Note: This is the EPIC-body title. The catalog calls this story "Package/Membership Tracking" (P2/5 points/Not Started) — see D-001.

EPIC-body status: Not Started (P2, 5 points) Catalog status: (catalog ID mismatch — see D-001) Verified status: Not Started — current SpaAppointment model supports single guest only; no groupId field, no couple-room-with-2-therapists scheduling logic, no group discount.

Code locations: NONE FOUND — no group/couple appointment code paths.

Unit + integration tests: none.

Flow tests: none.

Real-backend smokes: none.

Use cases referencing this story: none.

SME notes:

  • SpaRoom.type enum includes COUPLE (per CLAUDE.md Enums table), so the room model is ready — only the appointment-pairing layer is missing.
  • Honeymoon/anniversary couple bookings are a high-margin segment for Goa/Kerala resorts. The current platform forces receptionists to book two appointments back-to-back, then hope they land in the same room with paired therapists. Real-world: ~40% rate of mismatched timing or room assignment errors.

Drift findings: (none — story-vs-reality matches "Not Started")

  • [SME-PROPOSED] Author dependent story for the schema work: "Add groupId String? + groupRole String? (LEAD/PARTNER) + groupDiscountPercent Decimal? to SpaAppointment. Linked appointments share groupId; LEAD appointment carries the discount; COUPLE-type room MUST be assigned to all members of a 2-person group."

US-016-008 — Spa Revenue & Therapist Productivity Reports​

Note: This is the EPIC-body title. The catalog calls this story "Couple/Group Appointment Booking" (P3/3 points/Built) — see D-001.

EPIC-body status: Not Started (P1, 8 points) Catalog status: (catalog ID mismatch — see D-001) Verified status: Partial — A read-only ReportingFeedsController exists (/reporting-feeds/spa/sales-detail) feeding analytics-service LiveProxy via INTERNAL_SERVICE_TOKEN. NO RevPATH metric. NO therapist utilization aggregation. NO pre-built dashboard charts.

Code locations:

  • hmp-spa-service/src/reporting-feeds/ module — GET /reporting-feeds/spa/sales-detail?propertyId=&from=&to=&therapistId= returning SpaSalesDetailItem[]. Roles: SUPER_ADMIN, ADMIN, GM, SPA_MGR, MANAGER, ACCOUNTS, INTERNAL_SERVICE.
  • hmp-spa-service/src/reporting-feeds/reporting-feeds.controller.spec.ts — Controller spec exists.
  • (analytics-service consumes this via LiveProxyResolver per the spa CLAUDE.md)

Unit + integration tests: 1 controller spec on reporting-feeds.

Flow tests: none.

Real-backend smokes: none.

Use cases referencing this story: none.

SME notes:

  • The reporting-feeds module is the cleanest piece of Plan 2.2 architecture work — proper INTERNAL_SERVICE_TOKEN gating, scoped roles, sales-detail shape. But it's a raw feed, not a report. The EPIC body wants RevPATH (Revenue Per Available Therapist Hour) which is revenue / availableHours per therapist per period — that aggregation belongs in analytics-service.
  • "Built" claim in catalog (P3, 3 points, "Couple/Group Appointment Booking") is in fact "Not Started" (catalog conflation D-001 again). The EPIC body Not Started is correct for the reports surface but undersells the sales-detail feed which IS shipped.

Drift findings:

D-011 — [P2] [Cross-service-boundary + story-vague] Reports straddle spa-service (data feed) and analytics-service (aggregation + presentation). EPIC body says "Not Started" but the spa-service half (sales feed) IS shipped. Story body doesn't delineate the boundary — engineers will not know which service owns RevPATH calculation.

  • [SME-PROPOSED] Refine US-016-008 boundary: spa-service owns the sales-detail / appointments-detail read-feed endpoints under /reporting-feeds/spa/*; analytics-service owns the aggregations (RevPATH = revenue / availableHours; therapist utilization = bookedHours / availableHours; treatment popularity = COUNT(appointments) GROUP BY treatmentId). Dashboard chart components consume from analytics-service.
  • [SME-PROPOSED] Add AC: "Reports SHALL include India-context-aware filters: festival-window comparison (Diwali week vs same week prior year), monsoon-season comparison (Jun-Sep), wedding-season block (Nov-Feb)."

Note: This is the EPIC-body title. The catalog calls this story "Guest Spa Check-In" (P2/2 points/Built) — see D-001.

EPIC-body status: Not Started (P2, 5 points) Catalog status: (catalog ID mismatch — see D-001) Verified status: Not Started — no health-form code, no consent storage, no Ayurveda Prakriti assessment surface, no allergy-alert UX.

Code locations: NONE FOUND — no HealthForm, Consent, or Prakriti entities in spa schema.

Unit + integration tests: none.

Flow tests: none.

Real-backend smokes: none.

Use cases referencing this story: none.

SME notes:

  • Ayurveda-specific: Prakriti (body-constitution) assessment is a regulated practice — CTAA (Central Council for Ayurvedic Sciences) and Ayush Ministry require licensed practitioners to record Prakriti before any Panchakarma treatment. Skipping this opens the property to malpractice liability.
  • DPDPA art. 6 + 8 require explicit consent for processing health data (a "sensitive personal data" category in India). A digital consent form is the legal artifact required by DPDPA + the Information Technology (Reasonable Security Practices) Rules 2011.
  • Allergy data should NOT be stored on SpaAppointment.notes (free-text, not searchable, not audited). It needs its own model with retention rules per DPDPA art. 8(7) (delete on guest erasure — this is wired for the spa-service GuestErasedConsumer per the spa CLAUDE.md Consumers table, but consumer currently only erases guestName/guestEmail/guestPhone — does NOT handle health-form fields because they don't exist).

Drift findings: (none — story-vs-reality matches "Not Started")

  • [SME-PROPOSED] Author Prisma model: "SpaHealthForm { id, tenantId, guestId, propertyId, allergies String[], medicalConditions String[], pregnancyStatus enum, bloodPressureCondition enum, prakritiAssessment Json?, consentSignedAt, consentVersion, formData Json, createdAt } with @@unique([guestId, propertyId]). GuestErasedConsumer SHALL extend to null formData + prakritiAssessment on guest erasure (DPDPA compliance)."
  • [SME-PROPOSED] Add AC: "Before any first appointment for a guest, the spa receptionist SHALL collect/verify the digital health form. The appointment booking endpoint SHALL return 412 code=SPA_HEALTH_FORM_REQUIRED if no form exists for the guestId + propertyId pair. Override capability for emergency walk-ins gated to MANAGER role."
  • [SME-PROPOSED] Add AC: "Therapist UI SHALL surface allergies + medical conditions as a prominent banner on appointment-detail view, requiring explicit therapist acknowledgement before status can transition to IN_PROGRESS. Acknowledgement timestamped + stored in audit log."