EP-011: Booking Engine — Phase 1 Hardening
Phase 1 hardening sprint, 2026-04. Largest hardening footprint of any epic. Branches across
hmp-reservation-service,hmp-property-service,hmp-shared-packages, andhmp-booking-engine. Not yet pushed.
The booking engine had four shipping items in Phase 1: a complete xPrisma → ServiceClient migration (19 sites, the biggest single-epic migration in the sprint), mobile responsive polish, the introduction of standard loading/empty/error state primitives, and a WCAG 2.1 AA accessibility pass.
C4b — Booking-Engine xPrisma → ServiceClient Migration
Branch: feat/booking-engine-serviceclient (in hmp-reservation-service,
hmp-property-service, hmp-shared-packages)
What was fixed
The reservation service's booking engine code path read directly from roomType, room,
ratePlan, and property models that don't exist in its Prisma schema. This was the
root cause of multiple production crashes during seed runs in March 2026.
All 19 xPrisma sites migrated. Eight new endpoints introduced on property-service to own the reads:
| New endpoint | Replaces | xPrisma model |
|---|---|---|
GET /properties/:id/booking-engine/availability-snapshot | availability.service.ts snapshot logic | roomType, room, roomReservation |
GET /properties/:id/booking-engine/room-types | availability.service.ts room-type list | roomType |
GET /properties/:id/booking-engine/rate-plans | cart.service.ts rate-plan list | ratePlan |
GET /properties/:id/booking-engine/rate-plans/:id/with-rates | rate-plans.service.ts rate expansion | ratePlan, dailyRate |
GET /properties/:id/booking-engine/room-status-counts | availability.service.ts HK widget | room |
GET /properties/:id/booking-engine/room-count | capacity-ceiling check | room count |
GET /properties/:id/booking-engine/available-room | cart.service.ts room binding | room |
GET /properties/:id/booking-engine/room-types-with-rate-plans | initial-load convenience | roomType + ratePlan |
Full API spec: api/booking-engine-support-api.md.
CRITICAL security fix
Every new endpoint enforces assertPropertyTenant(propertyId, tenantId) before serving
data. This is a single SELECT cached for 5 minutes that verifies the requested property is
owned by the claimed tenant. The old xPrisma code did not perform this check on the
receiver side, which means a service-account caller could in theory have requested data
from any property. This is now closed.
PropertyServiceClient extensions
Eight new methods added in @hmp/service-clients/PropertyServiceClient. All use
AsyncLocalStorage for tenant/property forwarding and inherit the
INTERNAL_SERVICE_TOKEN injection from BaseServiceClient. See
architecture/cross-service-communication.md
for the full pattern.
Files touched
hmp-reservation-service/src/availability/availability.service.ts(5 sites)hmp-reservation-service/src/booking-engine/booking-engine.service.ts(8 sites)hmp-reservation-service/src/cart/cart.service.ts(4 sites)hmp-reservation-service/src/rate-plans/rate-plans.service.ts(2 sites)hmp-property-service/src/booking-engine-support/(new module: controller, service, module, 8 controller methods, 8 service methods, 8 DTOs)hmp-property-service/src/booking-engine-support/booking-engine-support.spec.ts(24 tests)hmp-shared-packages/packages/service-clients/src/PropertyServiceClient.ts(8 methods)
Tests
24 new property-service controller tests covering all 8 endpoints, success and error paths
including the assertPropertyTenant failure case. All passing.
D2 — Booking Engine Mobile Responsive
Branch: feat/booking-engine-mobile-polish (in hmp-booking-engine)
What was fixed
- New
Sheetprimitive (Radix UI Dialog wrapped as a bottom drawer) - New
useMediaQueryhook for breakpoint-aware rendering BookingSummaryrendered as a bottom drawer on mobile (vs. sidebar on desktop)- Payment method selector becomes a bottom sheet on mobile
- Sticky payment summary at the bottom of the viewport on mobile
- All interactive controls hit the 44×44 minimum touch target
- All input fields use
text-base(16px) to disable iOS auto-zoom - Calendar cells are 44×44 minimum on mobile
- Carousel images lazy-load and use srcSet for retina
Tests
- Playwright mobile suite: 23/23 pass
- Playwright desktop suite: 18/18 pass
- Playwright tablet suite: 18/18 pass
- 8 visual regression screenshots captured
D3 — Booking Engine Empty/Loading/Error States
Branch: feat/booking-engine-ui-states (in hmp-booking-engine)
What was added
Three new shared primitives in src/components/common/:
LoadingState.tsx— spinner + labelEmptyState.tsx— icon + heading + body + optional actionErrorState.tsx— icon + heading + retry action
Wired into 9 consumers (room list, rate panel, payment methods, search results, manage booking, etc.). 17 new unit tests covering rendering, ARIA semantics, and action callbacks. Mobile E2E suite still passes (24/24).
D7 — WCAG 2.1 AA Accessibility (Booking Engine)
Branch: feat/booking-engine-a11y-fixes (in hmp-booking-engine)
What was fixed
<html lang>attribute now syncs with i18next's active languagePaymentMethodscomponent uses an ARIA radiogroup with properaria-checkedsemanticsProgressIndicatoris a semantic<ol>instead of a div stack- aria-labels added to all icon-only buttons
- Screen-reader-only status prefixes added to dynamic regions
- Muted-foreground color tokens darkened to pass AA contrast
Known concerns
- Gold-on-ivory contrast failure:
#B8962E(brand gold) on#FAF8F0(ivory) measures 2.97:1, which fails AA (needs 4.5:1 for normal text). This affects accent text on the light theme. Design system decision needed — flagged inproject_phase2-phase3-scope.md.
Summary
| Story | Status before | Status after | Notes |
|---|---|---|---|
| US-011-01 (Room Search) | Partial | Built | C4b: xPrisma fix unblocked happy path |
| US-011-02 (Rate Display) | Partial | Built | C4b: rate plans now load |
| US-011-04 (Cart) | Partial | Built | C4b: room binding works |
| US-011-06 (Payment Flow) | Partial | Partial | D2/D7 polish; live Razorpay still pending |
| US-011-08 (Mobile Layout) | Not Started | Built | D2: full mobile responsive polish |
| US-011-12 (Loading States) | Not Started | Built | D3: state primitives shipped |
| US-011-15 (Accessibility) | Not Started | Partial | D7: 25 fixes; gold-on-ivory still fails |
Story-level status promotions: 5.