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+ livesrc/tree; review files underreviews/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 bothepics/EP-016-spa-wellness.md(live) and thereviews/EP-016/US-016-NNN-review.mdfiles 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
| Status | Meaning |
|---|---|
Built | Code exists + tests exist + tests pass |
Built-Untested | Code exists, no automated test coverage (or test exists but is a stub) |
Partial | Code exists for some acceptance criteria, gaps for others |
Stub | Code is a placeholder / TODO / returns mock data |
Not Started | No code |
Summary
| Metric | Count |
|---|---|
| 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-Built | 1 (US-016-003 — covered by 16-test scheduling.spec + 10-test cross-tenant + spa-booking.flow) |
| Built-Untested | 1 (US-016-005 — billing.service.ts hardened well beyond review verdict, but no end-to-end frontdesk→folio assertion in flow tests) |
| Partial | 4 (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) |
| Stub | 0 |
| Not Started | 3 (US-016-006 Prepaid Sessions; US-016-007 Couple/Group; US-016-008 Reports) |
| Drift findings | 11 (D-001..D-011; 4 P0 / 5 P1 / 2 P2) |
| [SME-PROPOSED] refinements | 18 |
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 viaisActive=falsehmp-spa-service/src/treatments/treatments.controller.ts—GET /spa/treatments,POST /spa/treatments,GET /spa/treatments/:id,PATCH /spa/treatments/:id,DELETE /spa/treatments/:idhmp-spa-service/src/treatments/dto/— CreateTreatmentDto, UpdateTreatmentDto, TreatmentFilterDtohmp-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 browserhmp-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 perPLAYBOOK.md).
Real-backend smokes:
hmp-web-dashboard/e2e/real-backend/smoke-spa.real.spec.ts— covers/spapage loading with seeded treatmentshmp-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.taxRatedefaults to18(GST 18% on spa services per Indian SAC 999722) perprisma/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/andpackages/) 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: "
SpaTreatmentSHALL carry a requiredsacCodefield (4-8 digit numeric, default 999722). All catalog browse responses (staff + guest-facing) SHALL includesacCode. Folio posting viaSPA_CHARGE_CREATEDSHALL forwardsacCodein payload (see D-008)." - [SME-PROPOSED] Add AC: "Treatment catalog SHALL enforce
@@unique([propertyId, name])(closes review GAP-2). Duplicate creates return 409code=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 slotshmp-spa-service/src/therapists/therapists.controller.ts— CRUD +GET /spa/therapists/:id/availabilityhmp-spa-service/src/therapists/availability.validator.ts— Custom@IsTherapistAvailabilitydecorator (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, TherapistFilterDtohmp-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.tsdoes 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
homeLanguageorgender-preference-acceptancefield. 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.tswith 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 Prismahas, 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_APPOINTMENTSwith 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: "
SpaAppointmentSHALL accept an optionalrequestedTherapistGenderenum (MALE|FEMALE|NO_PREFERENCE, defaultNO_PREFERENCE). When set, scheduling auto-assignment SHALL match only therapists whosegenderfield 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 viaSTATUS_TRANSITIONSmap; Kafka events on each transition. Cross-tenant guard:findUnique+ post-check throwingForbiddenExceptionon 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, AppointmentFilterDtohmp-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.tsdoes 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.findFirstandspaTherapist.findFirstfilter bytenantId(not justid). 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.tsbecause the canonicalappointments.service.spec.tsdoesn'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.findOneandupdateStatusto usefindFirst({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
NotFoundExceptionwhen 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 inpackage.jsonper the dashboard CLAUDE.md. - Color coding by treatment category aligns with
SpaTreatment.categoryenum (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 withdata-treatment-categoryattribute → 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); publishesSPA_CHARGE_CREATED; updatespaidAmount=totalAmount.hmp-spa-service/src/appointments/appointments.controller.ts:127-137—POST /spa/appointments/:id/folioroute delegates tobillingService.postToFolio()(no longer the stub the review file said it was).hmp-frontdesk-service/src/folios/consumers/spa-folio-charge.consumer.ts— Receiver creating theFolioTransactionrow.
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 spechmp-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
FolioTransactionfor e-invoice IRN generation. NIC IRP schema 1.1 rejects line items without HSN/SAC for B2B invoices > ₹500. - No
spa.charge.reversedevent, noPOST /spa/appointments/:id/folio/reverseendpoint. 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/:reservationIduntil the spa charge appears (≤2s p95) → assertFolioTransaction.amount === appointment.totalAmountandtransactionCode === '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.billingStateCodeagainstproperty.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(fromtreatment.sacCode, default 999722) andigst/cgst/sgstdecimal fields. Frontdesk-serviceSpaFolioChargeConsumerSHALL forwardsacCodeintoFolioTransaction.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/reverseSHALL emitSPA_CHARGE_REVERSED(with originalfolioTransactionIdif available) and resetappointment.paidAmount=0. Role-gated toMANAGER | ADMIN | SUPER_ADMIN. Idempotent — calling twice on the same appointment returns 409code=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 onSpaPackage+SpaPackageItemdefinitions (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.mdModels 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) — noPackagePurchase/PackageRedemption/PackageSessionUsageentities. 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 (
validUntilper 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 }andPackageSessionUsage { id, purchaseId, appointmentId, usedAt, sessionNumber }. Unique@@unique([purchaseId, sessionNumber])." - [SME-PROPOSED] Add AC: "When an appointment with
packagePurchaseIdlinkage is marked COMPLETED,BillingService.postToFolioSHALL be SKIPPED (no folio charge),usedSessionsSHALL increment by 1, andremainingSessionsSHALL decrement by 1. Exhausted package (remainingSessions=0) → 409 on next booking attempt with codeSPA_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.typeenum includesCOUPLE(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?toSpaAppointment. 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=returningSpaSalesDetailItem[]. 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 / availableHoursper 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)."
US-016-009 — Guest Health & Consent Form (Digital)
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-serviceGuestErasedConsumerper the spa CLAUDE.md Consumers table, but consumer currently only erasesguestName/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]).GuestErasedConsumerSHALL extend to nullformData+prakritiAssessmenton 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_REQUIREDif 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."