Skip to main content

Property #1 Launch Readiness Review — 2026-05-06

Reviewer: Claude (Opus 4.7, 1M context). Run timestamp: 2026-05-06 IST. Review duration: ~3.5 hours (Phase A inventory ≈ 30 min; Phase B 13 parallel flow sub-agents ≈ 90 min wall-clock; Phase C cross-cutting ≈ 30 min; Phase D synthesis ≈ 30 min). Working directory: C:\CLProjects\HMP. Methodology: every finding cited to file:line in the actual repository at the HEADs below. CLAUDE.md, MEMORY.md, prior platform-registry audits, and any "shipped"/"complete" claim in spec/plan files were treated as suspect and re-verified against code. PMS/OTA domain perspective applied throughout. No edits.

Repo HEADs at review time​

RepoHEADBranch
hmp-api-gatewayd343416master
hmp-auth-service5512770master
hmp-property-serviceeec9cfcmaster
hmp-reservation-servicecf29aa5master
hmp-guest-service0c1f179master
hmp-frontdesk-service95a5886master
hmp-housekeeping-service794e6a8master
hmp-pos-servicee09921dmaster
hmp-banquet-service01944b8master
hmp-channel-service6e35453master
hmp-notification-service17f122amaster
hmp-crm-service368d5f9master
hmp-analytics-service264564amaster
hmp-payment-service7a6d77amaster
hmp-spa-service74aef5dmaster
hmp-asset-service509ed53master
hmp-web-dashboard22b0f71master
hmp-admin-portalf5673a2master
hmp-booking-engine4713bcbfeature/be-goat-phase-8 (NOT master)
hmp-mobile-apps84fe56dmaster
hmp-kds-kiosk02f1b07master
hmp-shared-packages32885aemaster
hmp-infrastructured1b1b13master
hmp-docs240d904master

Note: hmp-booking-engine is on a feature branch. Production launch from master would not include the GoaT Phase 8 frontend work that the backend (already merged on master) was built to support.


§1 — Executive verdict​

NO-GO for property #1 launch. Of 13 minimum-viable launch flows, 11 are RED (block launch outright) and 2 are AMBER (significant gaps). 0 flows are GREEN. Every flow that involves money movement, regulatory compliance, or operational state machines has at least one P0 defect that would surface within the first week of real hotel operations.

Verdict counts​

VerdictCountFlows
GREEN0—
AMBER211 (ResAvenue), 12 (Payments)
RED111, 2, 3, 4, 5, 6, 7, 8, 9, 10, 13
UNKNOWN0—

Top 5 launch blockers (one line each)​

  1. Booking-engine race condition: two guests can both book the last room. No row lock, no unique constraint, no advisory lock anywhere in createBooking (hmp-reservation-service/src/booking-engine/booking-engine.service.ts:981-1068). Zero concurrency tests.
  2. Razorpay verifyPayment auto-confirms reservations with NO HMAC check when key is unset. booking-engine.service.ts:1741-1754, 1888-1938 — a property whose settings.razorpayKeyId is missing flips PENDING → CONFIRMED on a guest-supplied paymentId with no signature verification, no PaymentAttempt row, and a "mock fallback for dev/demo" branch one missing env-var away from production.
  3. Tape Chart hero feature missing context menu and all DetailPanel actions. hmp-web-dashboard/src/components/modules/front-desk/tape-chart-v2/tape-chart-v2-shell.tsx:365-372 shows literal 'coming in Phase 2' placeholder for every operation except "navigate to detail page". Right-clicking a bar in production does nothing. No virtualisation in v2 either (working code from v1 sits unused).
  4. Front-desk ID photo capture is broken end-to-end. Dashboard sends data:image/jpeg;base64,... URLs (hmp-web-dashboard/src/app/(dashboard)/front-desk/check-in/[id]/keynova.tsx:444-446, 966) into a DTO field decorated @IsUrl({require_protocol:true,protocols:['https','http']}) (hmp-frontdesk-service/src/checkin/dto/checkin.dto.ts:152-154). Validator rejects → 400. ID photo capture is functionally non-existent in production right now.
  5. Empty-nationality foreign guest bypasses FRRO entirely. hmp-frontdesk-service/src/checkin/checkin.service.ts:189-194 checks nationality.toLowerCase() === 'indian'/'india'/'in' and treats blank as Indian. The booking-engine path can omit nationality, so a foreign guest who self-registers without setting it skips Foreigners Act Form C requirement at check-in. Walk-in path has no FRRO enforcement at all.

Top 3 cross-cutting risks​

  1. build script silently swallows TypeScript errors. Every backend service's package.json runs tsc -p tsconfig.docker.json || true. A service with a real type error ships anyway. The || true should not exist. Hidden defect surface area is unknown.
  2. xPrisma anti-pattern still pervasive — 79 foreign-model Prisma reads in channel-service + reservation-service alone (prisma.roomType, prisma.ratePlan, prisma.property, prisma.guest). Local read-model-sync consumers exist but they're a parallel implementation of what @hmp/service-clients is supposed to be the canonical answer to. Event-bus drift between owner and reader is silent.
  3. EINVOICE_MOCK=true in QA (hmp-infrastructure/docker/docker-compose.qa-aws.yml:42) — the most regulated part of the platform (NIC IRN e-invoicing) is not exercised against the real NIC IRP in any pre-prod environment. Production has no boot guard against accidentally inheriting EINVOICE_MOCK=true. Compare to EmailService.onModuleInit which DOES hard-fail without SendGrid creds. A misconfigured prod deploy would issue invoices that look real but carry mock IRNs.

§2 — Flow status table​

#FlowVerdictP0 countTop blocker
1Tenant onboardingRED8Subscription row never written to typed table; legacy code path writes JSON blob. Onboarding never imports the SubscriptionsModule.
2Property onboarding (7 sub-flows)RED8+Booking engine room cards use hardcoded mock data, not API; Google Maps autocomplete absent; tax slab boundaries hard-coded; bulkCreate has cross-tenant insertion bug.
3Booking engine integrationRED2Last-room race + payment auto-confirm-without-HMAC.
4Staff verifies reservationRED5Folio tab is client-side fabrication; search box ignores typed text; Modify/Reg-card buttons dead; Cmd+K calls non-existent route; xPrisma guest reads.
5Front-desk / Housekeeping updatesRED6Two /room-move controllers; no folio realignment on move; no availability check on extend; mobile housekeeper photo-proof flow broken.
6Front-desk check-in (IN+INTL)RED7ID photo data: URL rejected by validator; empty-nationality bypasses FRRO; express check-in is a stub; walk-in skips FRRO; police register PII feed unimplemented.
7Room service to folioRED3V2 GST split silently dropped at frontdesk receiver; MenuItem has no HSN/SAC; service-charge GST hard-coded 0.18.
8Guest check-out & invoiceRED6IRN ack number overwrites sequential invoice number; two divergent invoice generators; no production guard against EINVOICE_MOCK; no split-payment; no invoice attachment on email.
9Night auditRED8No @@unique([propertyId, auditDate]) constraint; dedup window timezone math wrong; rate read by averaging not from rate calendar; no cancellation-policy-aware no-show; no POS day-close cascade; no event outbox.
10Reports & dashboardsRED4KPI tile reportIds wrong (every Operations Hub home tile 404s); reports hard-coded UTC not property TZ; roomsSold increments per booking not per night.
11ResAvenue channel managerAMBER4Webhook idempotency race-vulnerable; itemsProcessed always 1; stay-only restrictions silently dropped; no DB-level idempotency on retries.
12POS terminal & paymentsAMBER4Razorpay terminal adapter still sends defunct X-Payment-Idempotency-Key; PineLabs batch-settlement is no-op (no HMP-side reconciliation); refund modal absent; subscription flag in CLAUDE.md doesn't exist in code.
13Tape Chart CalendarRED7No context menu wired; DetailPanel actions are placeholder toasts; no virtualisation in v2 production shell; no optimistic concurrency; no mobile/a11y; only 5 e2e smokes.

Total P0 findings across the 13 flows: ≈ 73.


§3 — Per-flow detailed findings​

The following 13 sections are the verbatim outputs of the 13 parallel flow sub-agents, lightly normalised for heading consistency. Each finding cites file:line and was produced by independent investigation.


Flow 1 — Tenant Onboarding​

Verdict: RED. Two divergent signup paths with different validation rules; no email verification; no audit trail on tenant lifecycle; subscription row never written to the typed table; default property data is a documented stub; MFA never enrolled for ADMIN; no DPDPA data-residency support.

Flow definition. (1) Self-service signup — POST /api/tenants/signup → TenantsController.signup (hmp-auth-service/src/tenants/tenants.controller.ts:34-56). (2) Admin-driven create — POST /api/tenants (tenants.controller.ts:58-72) — same code path. (3) Onboarding wizard (ADMIN/SUPER_ADMIN) — POST /api/onboarding/start | /:tenantId/property | /:tenantId/rooms | /:tenantId/activate (onboarding.controller.ts:53-167) — calls property-service via PropertyServiceClient. (4) Subscription activation via legacy BillingModule.SubscriptionService.createSubscription, NOT the typed-row subscriptions/subscription.service.ts. (5) Login — POST /api/auth/login (no email-verification gate).

Data layer.

  • [P0] Tenant model has no legalName, tradeName, pan, or dataResidency columns (hmp-auth-service/prisma/schema.prisma:19-58). Indian compliance demands legal-name vs trade-name separation; both are missing. PAN is required for B2B GST invoices.
  • [P0] No TenantStatus enum. Lifecycle is a single boolean Tenant.isActive (schema.prisma:25). Suspension stores suspendedAt/suspendReason inside settings JSON (tenants.service.ts:240) — unindexed, untyped. No PENDING (e.g., awaiting email verification) and no CANCELLED state.
  • [P0] User.role defaults STAFF but signup hardcodes ADMIN (tenants.service.ts:80, onboarding.service.ts:136). MFA columns exist (mfaEnabled, mfaSecret — schema.prisma:103-107) but no MFA-enrolment hook in onboarding.
  • [P1] Tenant.billingContactName/Email/Phone exist (schema.prisma:40-42) but are not exposed in CreateTenantDto/UpdateTenantDto (dto/create-tenant.dto.ts); cannot be set during signup or update at all. SubscriptionService.createSubscription reads these (subscriptions/subscription.service.ts:138-142) — silent fallback to supportEmail or billing+{code}@keynovahotels.com.
  • [P1] TenantSubscription.status enum has TRIAL/ACTIVE/PAST_DUE/... (schema.prisma:406) but the legacy path used by onboarding writes status into tenant.settings.subscription JSON instead (billing/subscription.service.ts:215, 824-846).

Backend.

  • Gateway two-file rule for /tenants (config services.config.ts:19; controllers proxy.controller.ts:32, 39): PASS.
  • Gateway two-file rule for /onboarding (services.config.ts:54; proxy.controller.ts:107, 114): PASS.
  • [P2] Gateway /auth parent path missing decorator — proxy.controller.ts:23 only has @All('auth/*'). Bare POST /api/auth would 404. Inconsistent with platform two-file rule.
  • [P0] TenantsController.signup is @Public() (tenants.controller.ts:33) — no rate limit specific to signup beyond the gateway's default-write 60/min bucket. Account-creation endpoints normally need a stricter bucket.
  • OnboardingController routes are gated @Roles('SUPER_ADMIN','ADMIN') (onboarding.controller.ts:53,83,112,141,170) and the assertTenantScope helper enforces tenant isolation for ADMIN: PASS.
  • [P0] POST /tenants/:id/suspend and /reactivate (tenants.controller.ts:188, 206) only flip isActive; no audit row written, no Kafka event published, no session invalidation for already-issued JWTs.

Cross-service / events.

  • [P0] Tenant create publishes TENANT_CREATED Kafka event (tenants.service.ts:97-111). OnboardingService.startOnboarding does NOT publish TENANT_CREATED (onboarding.service.ts:110-145). Welcome-email consumer (hmp-notification-service/src/dispatcher/dispatcher.service.ts:826-878) only fires for the /tenants/signup path.
  • [P0] OnboardingService.activateSubscription (onboarding.service.ts:386-428) injects the legacy BillingModule.SubscriptionService. That class persists subscription data into tenant.settings.subscription JSON, NOT into the TenantSubscription Prisma table. The newer subscriptions/subscription.service.ts is wired into a parallel module that onboarding does not consume.
  • [P0] SUBSCRIPTION_VIA_PAYMENT_SERVICE_ENABLED flag is mentioned in root CLAUDE.md but never read by OnboardingService.activateSubscription. Onboarding hardcodes the legacy SubscriptionService.
  • [P0] OnboardingService.addProperty calls PropertyServiceClient.seedPropertyDefaults, which is a documented stub returning zeros (hmp-shared-packages/packages/service-clients/src/lib/property-service.client.ts:530-550). New tenants get zero default room types / rate plans / outlets / tax config.
  • [P0] No email-verification flow exists. Grep verifyEmail|EMAIL_VERIFICATION|emailVerified across hmp-auth-service/src returns zero matches. User model has no emailVerified column.
  • [P0] No audit-log write in TenantsService.signup, suspend, reactivate, or any OnboardingService method. The AuditLog model (schema.prisma:230-249) is not populated for tenant lifecycle events. Hash-chained tamper-evident audit (advertised in service CLAUDE.md) is absent on this flow.

Frontend.

  • [P0] Admin-portal signup page client-side password rule is >= 8 chars + 1 upper + 1 digit (hmp-admin-portal/src/app/(auth)/signup/page.tsx:68-78); backend requires >= 12 chars + 1 upper + 1 lower + 1 digit + 1 special (tenants/dto/create-tenant.dto.ts:71). Frontend will accept passwords the backend rejects, surfacing as a 400 only after submit.
  • [P0] OnboardingDto.password requires MinLength(8) (onboarding/dto/onboarding.dto.ts:48) but OnboardingService.startOnboarding calls validatePasswordStrength (onboarding.service.ts:103) which enforces 12. DTO and runtime check disagree.
  • [P1] Tenant detail page hmp-admin-portal/src/app/(admin)/tenants/[id]/page.tsx:395 displays GSTIN read-only. legalName, tradeName, billingContactName/Email/Phone, dataResidency are not editable.
  • [P1] Dashboard has no "My Organization" / tenant-edit page (no organization/ or tenant/ directory under hmp-web-dashboard/src/app/(dashboard)/settings/). Tenant ADMINs cannot self-edit GSTIN, contact info, or billing contact.

Test coverage.

  • hmp-auth-service/src/tenants/tenants.service.spec.ts — 27 tests; zero signup happy-path tests, zero signup Kafka-publish tests, zero negative-path tests for duplicate email or duplicate code on the signup path.
  • onboarding.service.spec.ts — 22 tests; no tests for password rule mismatch, no tests for seedPropertyDefaults stub returning zeros, no tests asserting TenantSubscription row is written.
  • No e2e Playwright test exercises the full signup → onboarding wizard → subscription path.

Domain correctness.

  • [P0] Tenant.code @unique but slugify("My Hotel ™") strips Unicode. User.@@unique([tenantId, email]) allows same email across tenants. OnboardingService.startOnboarding rejects same-email-cross-tenant via global findFirst (onboarding.service.ts:87-95); TenantsService.signup does NOT (tenants.service.ts:72-87). Two onboarding entry points, different uniqueness semantics.
  • [P0] DPDPA / data residency: zero schema or code support. No dataResidency column, no per-tenant DB partitioning, no RLS middleware on this flow.
  • [P0] signup skips GSTIN pre-check (tenants.service.ts:62-70); update path does it (tenants.service.ts:194-200). Duplicate GSTIN at signup produces generic Prisma P2002 instead of the friendly 409.
  • [P0] MFA never enrolled for ADMIN at create time. Contradicts platform CLAUDE.md "MFA TOTP enforced for ADMIN/SUPER_ADMIN."
  • [P0] Tenant suspension blast radius: only flips isActive=false, no session invalidation. A user logged in 14 minutes before suspend retains access for up to 15 min.

Flow 2 — Property Onboarding​

Verdict: RED. Flow 2 fails the audit's mandatory checks in all 7 sub-flows.

Data layer.

  • [P0] Property latitude/longitude typed as Float? not Decimal — hmp-property-service/prisma/schema.prisma:115-116. IEEE-754 floats lose precision at the 7th decimal (~1cm). Needed: @db.Decimal(10,7).
  • [P0] PIN code field is postalCode String? with no length/format validation — prisma/schema.prisma:96 and src/properties/dto/property.dto.ts:101-103, 289-292. India 6-digit PIN check absent.
  • [P0] RatePlan has no currency column — prisma/schema.prisma:359-397. Multi-property-different-currency tenants cannot run cross-currency rate plans.
  • [P0] CancellationPolicy.rules is untyped JSON — prisma/schema.prisma:474. Zod helper exists at src/cancellation-policies/dto/cancellation-rules-validator.ts but it is application-level only.
  • [P0] Tax has no slab-rule column — prisma/schema.prisma:521-544. Indian GST tiering (12% if room < ₹7,500/night vs 18% above) is hard-coded in src/tax/tax-rule-engine.service.ts and worked around by creating two TaxConfiguration rows with different SAC codes (996311 vs 996312) in src/tax/tax.controller.ts:103-131. Slab boundary changes require code deploy.
  • [P0] inclusive Boolean not honored by tax engine — gotcha 6 in src/tax/CLAUDE.md admits "TaxRuleEngineService does NOT yet factor inclusive into the calculation". Tax-inclusive rate plans (common with OTAs) will double-tax.
  • [P1] RoomType.amenities is String[] (prisma/schema.prisma:245) — audit asked for typed {icon, label}. Property-level amenities Json is structured (:129) but room-type level is plain array. Not booking-engine renderable as iconified chips.
  • [P1] RoomType.images typed Json @default("[]") — duplicates the PropertyImage table. No FK, no S3-key tracking.
  • [P1] RatePlanRate is row-per-(plan×roomType×date) — prisma/schema.prisma:419-443. 365 days × 5 room types × 5 plans = 9,125 rows per property. No range/RLE compression.
  • [P2] RateRestriction and ratePlan.settings.restrictions[] JSON both carry restriction state. Two writers, one truth — drift.

Backend.

  • [P0] Gateway two-file rule violation: /extra-charges and /property-taxes registered in services.config.ts:1055,1063 but only property-taxes has @All decorators in proxy.controller.ts:2258,2260; /extra-charges is completely missing from proxy.controller.ts. /seasons is missing from BOTH files. Root CLAUDE.md warning: "Missing either → 404 before the config is even consulted."
  • [P0] RoomsService.bulkCreate has no tenant ownership check — hmp-property-service/src/rooms/rooms.service.ts:61-100. Only validates Property.id exists, never asserts the property's tenantId === callerTenantId. The assertRoomTenant helper is used by update/updateNotes/updateStatus/remove but not by create or bulkCreate. Cross-tenant room insertion bug.
  • [P0] bulkCreate capped at 100 — src/rooms/dto/bulk-create-rooms.dto.ts:23 @Max(100). A 200-room hotel needs two requests. No idempotency key on the bulk endpoint.
  • [P0] No bulk-rooms UI in dashboard — Grep "/rooms/bulk" in hmp-web-dashboard/src returns zero. Rooms settings page only has single-room create dialog. Audit requirement "must support 100-room property without 100 manual clicks" — fails outright.
  • [P1] applyIndianGstDefaults derives stateCode from GSTIN prefix — src/tax/tax.controller.ts:97 — but skips validating GSTIN's state matches property's state field.
  • [P1] No e-invoice GSTIN-of-counter-party handling in property-service — hmp-frontdesk-service/src/einvoice/einvoice.module.ts:79-124 shows the provider is mocked by default (EINVOICE_MOCK=true).

Frontend.

  • [P0] No Google Maps Places autocomplete on property settings — hmp-web-dashboard/src/app/(dashboard)/settings/property/page.tsx:1176-1228 shows manual <Input type="number"> for lat/lon. Grep "Places|Autocomplete|google.maps" returns nothing relevant.
  • [P0] No embedded map preview on property settings. googleMapsUrl field is not even rendered in the settings UI.
  • [P0] Booking engine room cards use hardcoded mock data — hmp-booking-engine/src/pages/direction-b/RoomDetailPage.tsx:74-99 — ROOM_TYPES is a static array with Unsplash URLs (:152-156) and hardcoded amenity strings. Cancellation/refund text is hardcoded literal "Free cancellation until 23 Apr" (:558). NOT API-driven. Not consuming property-service RoomType or CancellationPolicy.rules at all.

Indian / domain.

  • [P0] Indian GST slab boundaries (<=7500=12%, >7500=18%) hard-coded in code, not config — src/tax/CLAUDE.md gotcha 2.
  • [P0] Property.gstin not state-aware — prisma/schema.prisma:102 is a single string. India multi-state chains hold one GSTIN per state. Per-state GSTIN is on TaxConfiguration.gstinNumber (:527), but folio printing pulls from property.gstin — multi-state property prints wrong GSTIN.

Tests. 53 spec files in property-service — but no spec for bulkCreate tenant-ownership (the missing check), no spec asserting RatePlanRate row-write count, no spec for inclusive flag actually changing tax math.


Flow 3 — Booking Engine Integration​

Verdict: RED. Two ship-blocking defects.

Flow definition. Guest opens the Vite booking widget, hits search → selects a room → submits guest+payment → reservation appears on the dashboard list. Implemented via API-key-gated @Public() controller on reservation-service (/api/booking-engine/*), with payment going either through legacy in-process Razorpay (default) or proxied to payment-service when build flag is on.

Data layer.

  • [P0] No inventory lock at booking commit — hmp-reservation-service/src/booking-engine/booking-engine.service.ts:981-1068 — createBooking opens a prisma.$transaction that re-reads no inventory and creates Reservation + RoomReservation rows unconditionally. No SELECT FOR UPDATE, no advisory lock, no @@unique([propertyId, roomTypeId, date]) row. Grep forUpdate|advisory|Serializable across the entire repo: zero matches. Two guests booking the last room at the same instant both succeed.
  • [P0] RoomReservation carries no per-night uniqueness — prisma/schema.prisma:187. Inventory is computed by counting RoomReservation rows post-hoc. Combined with the missing lock, no DB-level safety net.
  • [P1] Reservation.holdReleaseDate is @db.Date but holdReleaseTime is String "HH:mm" — prisma/schema.prisma:74-75. Scheduler at src/reservations/hold-release.scheduler.ts:67-71 reconstitutes the Date with setHours(...) in server local time (no UTC marker), so the 30-min payment window can drift hours across timezone-heterogeneous deployments.
  • [P1] Idempotency-Key is supported on POST /booking-engine/reservations (admin-style payload) but the actual frontend route POST /booking-engine/bookings (createBookingFromEngine) calls createBooking with no idempotencyKey — booking-engine.service.ts:1508. Guest double-clicks duplicate the reservation.

Backend endpoints.

  • [P0] Search/availability/calendar are @SetMetadata(REQUIRE_API_KEY, false) on the @RequireApiKey() controller — booking-engine.controller.ts:71, 86, 94, 104, 263, 273, 287 — these endpoints accept ZERO authentication. Search remains fully open and DoS-able by anyone who knows a propertyId.
  • [P1] Server-side amount cross-check uses Math.abs(params.amount - serverAmount) > 1 — booking-engine.service.ts:1682, 1797 — guest can underpay by ₹1 silently.
  • Two-file rule satisfied: services.config.ts:1213 /booking-engine, :1221 /booking-trips, :508 /payments; matching decorators in proxy.controller.ts.

Cross-service & events.

  • [P0] Two parallel payment paths exist; legacy is default. booking-engine.service.ts:1660-1755 (initiatePayment) reads razorpayKeyId/razorpayKeySecret from property.settings JSON with a "mock fallback for dev/demo" at :1741-1754. If credentials are absent, the booking is "confirmed" against a fake order_… ID. verifyPayment at :1888-1908 only calls crypto.createHmac when credentials exist — when they don't, the if block is skipped entirely and the reservation flips PENDING → CONFIRMED with NO signature check (:1925-1938). Critical PCI/business-logic hole.
  • [P1] sendBookingConfirmation (booking-engine.service.ts:2372-2394) publishes EventTopics.NOTIFICATION_SEND with type: 'BOOKING_CONFIRMATION_EMAIL' — that string is NOT a registered event-code. The actual confirmation email is fired by RESERVATION_CREATED consumed by dispatcher. The notification call is functionally redundant.

Frontend.

  • [P1] Default API key fallback 'hmp-default-public-booking-engine-key' — hmp-booking-engine/src/lib/api/client.ts:23. DEV-only warning; production cliff is at POST /reservations.
  • [P1] bookingApi.calculatePricing is @deprecated-throws (bookingApi.ts:75-81). searchAvailability's pricePerNight is BAR-only; contract rate adds a rateQuote field but the frontend's CreateBookingRequest does NOT include rateAgreementId. A guest who sees a contract rate at search will be charged BAR at booking commit. Quote-to-book drift.

Tests.

  • [P0] Zero concurrency tests. Grep "concurrent|race|two guests|last room|simultaneous" across reservation-service: only one comment-level hit. No test asserts two parallel createBooking calls for the last available room produces exactly one CONFIRMED reservation.
  • [P1] HoldReleaseScheduler has no test — hold-release.scheduler.spec.ts does not exist.

Domain correctness.

  • [P1] GST stacked onto totalAmount per-room (booking-engine.service.ts:927-931) — but the OTA path uses RoomReservationDto.isTaxInclusive (per CLAUDE.md gotcha 7) which the booking-engine path does NOT set. If a hotel ever ingests both flows, double-GST possible.
  • [P1] room.assignment in createBooking (:1071-1110) is "best-effort, does not fail the booking" — a property with no available physical room creates a CONFIRMED reservation with RoomReservation.roomId = null. Check-in flow then must scramble.
  • [P2] paymentStatus is String? not enum (schema.prisma:99) — typo silently produces garbage.

Flow 4 — Staff Verifies Reservation​

Verdict: RED (NO-GO). The detail page's folio is a 3-line client-side fabrication; guest panel reads from a possibly-stale local Prisma read-model instead of GuestServiceClient; list-page search box passes filter values into a DTO field the backend ignores; global Cmd+K calls a non-existent route; Modify/Reg card buttons are dead.

Backend endpoints.

  • List/search — POST /reservations/search at hmp-reservation-service/src/reservations/reservations.controller.ts:433-461. Implementation reservations.service.ts:2210-2302.
  • Detail — GET /reservations/:id at reservations.controller.ts:262, findOne at reservations.service.ts:1421-1464. Eagerly includes property, guest, company, group, roomReservations, folios, payments, reservationHistory.
  • Modify — PUT /reservations/:id at reservations.controller.ts:300. Invalidates availability:{propertyId}:* Redis at reservations.service.ts:1780.
  • History — GET /reservations/:id/history at :370.
  • Folio (cross-service) — NOT called from the detail page. Dashboard's frontDeskApi.getFolios(reservationId) (hmp-web-dashboard/src/lib/api/front-desk.ts:225-234) hits GET /folios/by-reservation/:id, but the detail page never invokes it.

Cross-service reads.

  • [P0] Guest profile served from local Prisma Guest read-model, NOT GuestServiceClient.getGuest — reservations.service.ts:1438 include: { guest: true }. Schema declares full Guest model at hmp-reservation-service/prisma/schema.prisma:371-393. The detail page renders res.guest.email, res.guest.phone, res.guest.totalStays, res.guest.vipLevel (keynova.tsx:1141-1145, 925, 1344) directly from this stale read-model. Charter violated.
  • [P0] Same issue on search and findAll: reservations.service.ts:2286, 1339, 1399, 1665 all include: { guest: true }.
  • [P0] Folio tab on detail page is fabricated client-side, not fetched. keynova.tsx:947-992 builds 3 synthetic FolioLine rows from res.totalAmount, res.taxAmount, res.paidAmount. NO call to frontDeskApi.getFolios(id). Real folio rows in frontdesk-service (POS charges, mini-bar, manual charges, multiple payments) are invisible.

Frontend.

  • [P0] Search box is broken end-to-end. keynova.tsx:217 passes guestName: debouncedSearch into ReservationList. Filter flows through reservationApi.list() at lib/api/reservations.ts:124 which routes query: filters.guestName || filters.confirmationNumber (clobbers guestName to query). Backend search() reads searchDto.guestName and searchDto.confirmationNumber; it never reads searchDto.query. Typing in the search box produces zero filtering.
  • [P0] Modify and Reg card buttons are dead — no onClick (keynova.tsx:1388-1393).
  • [P1] Keys / KeySetupGrid (keynova.tsx:218-279, 1473-1481) is mock-only.
  • [P1] Travel party (keynova.tsx:996-1007) synthesises Adult guest 2, Child 1 strings — does NOT read PaxRecord (the actual per-pax model).
  • [P1] Hero "Activity · last 48h" TimelineRail (keynova.tsx:1273-1292) hardcodes 2 events — does not read reservationHistory.

Domain correctness.

  • [P0] Global search calls non-existent route. apiClient.get('/search') (hmp-web-dashboard/src/lib/hooks/use-global-search.ts:85) — no /search registered in gateway services.config.ts, no service exposes @Get('search') matching the federated shape. Cmd+K returns 404 in production.
  • [P1] reservation.totalAmount in "Rate & policy" card re-derives pre-tax via pro-rate hack (tax * rateAmount) / total — fragile across multi-room reservations with mixed GST slabs.

Tests. Unit: __tests__/reservations-page.test.tsx exists (single file). Detail page has NO unit tests. E2E: 5 specs exist (list, detail, amend, cancel, create) — they did not catch the broken search-box wiring or dead Modify button (both bugs are in production).


Flow 5 — Front-desk / Housekeeping Updates​

Verdict: RED (NOT launch-ready). The HK gateway auth and tape-chart drag wiring are good. Everything else in this flow violates the audit charter.

Data layer.

  • [P0] No RoomMove model. Audit trail lives in ReservationHistory.action='ROOM_MOVE' only. No dedicated history table with fromRoomId/toRoomId/movedAt/movedBy — every move requires JSON blob spelunking.
  • [P0] No retroactive folio rate adjustment on move. roomMove() and roomUpgrade() mutate RoomReservation.roomId (and rateAmount only when dto.adjustRate is set) but never split the existing folio room-charge line. room-ops.service.ts:118-122 updates the row in place; no folio CHARGE/ADJUSTMENT is posted. Industry rule violated.
  • [P0] No availability re-check on extend. reservations.service.ts:1606-1647 recomputes totalAmount for the new night count but never queries inventory for the added nights — you can extend into a sold-out date.
  • [P0] No channel-manager push on extend. reservations.service.ts:1772-1778 publishes RESERVATION_MODIFIED. Channel-service has no ReservationModifiedConsumer that pushes inventory deltas back to OTAs.
  • [P1] No cancellation-policy timer reset on extend.

Backend endpoints.

  • [P0] Two /room-move POST handlers. checkin.controller.ts:97 (top-level, takes x-property-id header) and room-ops.controller.ts:13 (@Controller('front-desk'), no header). Dashboard calls apiClient.post('/room-move', dto) → gateway @All('room-move') (proxy.controller.ts:517) → frontdesk → hits the inferior checkin.controller.ts handler. The room-ops one with the better validated code is reachable only via /front-desk/room-move. Two implementations, divergent behavior.
  • [P0] No /extend-stay endpoint. Extend is implemented as PUT /reservations/:id { departureDate }. The seven required steps in the audit checklist (availability+rate+policy+OTA-push) are not in one place.
  • [P0] EXTENSION_NIGHT folio charge propagates over Kafka, not in-transaction. reservations.service.ts:1744-1765 publishes FOLIO_CHARGE_POSTED outside the DB transaction (comment at :1723-1726 admits "no transactional outbox"). At-least-once delivery; if reservation-service writes succeed but Kafka publish fails permanently, balance under-bills silently.
  • [P0] PosFolioChargeConsumer is default-OFF behind ENABLE_POS_FOLIO_CHARGE_CONSUMER=true (pos-folio-charge.consumer.ts:70). If staging/prod miss the flag, no extension-night CHARGE row ever lands on the folio.
  • [P0] Room move uses xPrisma cross-service direct DB access. room-ops.service.ts:15-29, :65-72 reads reservation/roomReservation from a slim per-service Prisma schema that doesn't carry the model — STRICT-TODO left explicit.

WebSocket auth. HousekeepingGateway (gateway/housekeeping.gateway.ts:97-174) verifies JWT in handleConnection, decodes alg from header (:114-123) instead of trusting client claim, requires RS256 in production with JWT_PUBLIC_KEY (:127-138), HS256 fallback only in dev. Tenant guard at :269-275, property guard at :279-285. Solid — the highest-quality piece of Flow 5.

Frontend.

  • Tape-chart drag → room-move: wired (tape-chart/drag/use-drag-reservation.ts:9, :71).
  • Reservation detail "Extend" button: navigates to /front-desk/calendar?action=extend (reservations/[id]/keynova.tsx:936) — does NOT call any extend endpoint directly.
  • [P0] Mobile housekeeping task page (hmp-mobile-apps/apps/housekeeping/app/task/[id].tsx): only renders task.photos.length count; has no <PhotoUpload taskId> widget. PhotoUpload is wired only to room/[id] with a roomId prop. Mark Complete (:175-184) calls updateTaskStatus(id, 'COMPLETED') directly — when property has requireInspectionPhoto=true, mobile housekeeper hits the HK_INSPECTION_PHOTO_REQUIRED safeguard (tasks.service.ts:653-665) and is blocked with no UI path to attach a photo. Critical mobile flow break.

Tests. room-ops.service.spec.ts (11), upgrades.service.spec.ts (11), tasks.service.spec.ts (11), housekeeping.gateway.spec.ts (15). No coverage for retroactive rate split, OTA push, cancellation-policy reset, or end-to-end EXTENSION_NIGHT bus integration.

Domain correctness — PMS. Room move destructive without folio realignment. Extend can oversell. No OTA push on extend. EXTENSION_NIGHT consumer opt-in. Mobile photo path broken. Two /room-move controllers. Discrepancy detection misses sleeper case. Room-status board hard-codes doNotDisturb: false (rooms.service.ts:196) — DND flag is dead.


Flow 6 — Front-desk Check-in​

Verdict: RED (NO-GO). Multiple P0 defects across both Indian and International tracks.

Data layer.

  • Folio (hmp-frontdesk-service/prisma/schema.prisma:20-62) opened on check-in via tx.folio.create at checkin.service.ts:374-382. Single folio per reservation; no per-pax folios.
  • GuestIdVerification (schema.prisma:860-877) — persisted fire-and-forget via setImmediate after the response is sent (checkin.service.ts:515-532, 593-609). If tenantId cannot be resolved, the row is silently skipped with a WARN — a successful check-in can return 201 with NO ID-verification record persisted.
  • KeyCardLog (schema.prisma:304-324) present, but KeyCardService.encodeForCheckIn does NOT write a row; writes only happen in the hardware adapters. No persistent key-card audit trail.
  • Guest-service GuestIdentity (hmp-guest-service/prisma/schema.prisma:203-219): idType enum supports PASSPORT/AADHAAR/etc. idNumber String plaintext column, application-encrypted via FieldEncryptionService in guests.service.ts:273. Aadhaar masked on read to last-4 (guests.service.ts:90-93). 12-digit regex at :104 — no Verhoeff checksum.

Backend endpoints.

  • POST /check-in/:reservationId (checkin.controller.ts:58-84).
  • POST /check-in/batch (max 50).
  • [P0] POST /express-check-in is a stub — only returns {assignedRoom, message: "Room assigned. Complete check-in at kiosk..."} (checkin.service.ts:691-752). Does NOT flip reservation status, open folio, persist ID, or enforce FRRO. RED for international guests at kiosks.
  • [P0] POST /walk-in (:115) — has its own duplicate-FRRO-bypass: WalkInDto (dto/checkin.dto.ts:212-278) accepts idDocumentType/idDocumentNumber but has no formC field and no FRRO enforcement code in walkIn().
  • C-Form: POST /:reservationId/guest/:guestId generate; GET /pending-submission 24h overdue digest; GET /:id/pdf; POST /:id/mark-submitted.
  • Gateway routing two-file rule verified at services.config.ts:261-351 and proxy.controller.ts:449, 457, 486, 493/499, 505/511, 2027/2029, 2145/2151. PASS.

Compliance — Form C / FRRO / Aadhaar / DPDPA.

  • [P0] purposeOfVisit hardcoded "Tourism" (cform.service.ts:319); arrivedFrom hardcoded empty string (:318). Dashboard never collects either. The dashboard's formC.arrivedFrom/purposeOfVisit is DROPPED — generateCForm() builds a fresh cform object from guest-service data. arrivedFrom will be empty in every generated PDF.
  • [P0] No visa type captured into the generated C-Form. cform.service.ts:312 writes visaType: '' regardless of input.
  • [P1] dateOfBirth and gender pulled from guest-service — foreign guest created via booking engine without DOB/gender will have empty fields on FRRO form — FRRO will reject.
  • [P0] FRRO submission is ManualUploadFrroClient (hmp-shared-packages/packages/integrations/src/lib/frro/manual-upload-frro-client.ts:29-54): submitFormC returns hardcoded portal URL and synthesised MANUAL-<ts>-<last4> reference. No real e-FRRO API integration.
  • [P0] Foreigner detection naive: (guest.nationality || '').trim().toLowerCase() === 'indian'/'india'/'in' (checkin.service.ts:189-194). A foreign guest with nationality='' is treated as Indian — silently bypasses FRRO. The booking-engine flow can omit nationality entirely.

Aadhaar / DPDPA.

  • Aadhaar storage: GuestIdentity.idNumber application-encrypted, masked on read to last-4. PASS for storage.
  • [P1] GuestIdVerification.idDocumentNumber (frontdesk-service) — falls back to plaintext with a WARN (checkin.service.ts:91-95) on misconfigured prod env (no ENCRYPTION_KEY). Should hard-fail in prod.
  • [P1] DPDPA 90-day retention: only fires on RESERVATION_CHECKED_OUT for guest-service rows — GuestIdVerification rows in frontdesk-service have no retention timer.

State guest-register.

  • [P0] GET /reporting-feeds/frontdesk/police-register (reporting-feeds.service.ts:409-464) is a stub — :458: passportOrIdNumber = null; // Would decrypt formCDataEncrypted in production. Not implemented. State guest-register cannot be filed.

Frontend.

  • [P0] DTO validator rejection — dashboard sends data:image/jpeg;base64,... URL into idDocumentImageUrl (hmp-web-dashboard/src/app/(dashboard)/front-desk/check-in/[id]/keynova.tsx:444-446, 966). CheckInDto.idDocumentImageUrl is @IsUrl({require_protocol:true,protocols:['https','http']}) (dto/checkin.dto.ts:152-154). Validator FAILS → 400. Same defect for signatureImageUrl. No client-side upload to S3 before posting; no presigned-URL flow. Check-in is broken end-to-end for any prop with image capture.
  • [P0] Field name mismatch: dashboard sends signature (front-desk.ts:37) — backend DTO expects signatureImageUrl. Signature silently dropped by validation pipe.
  • [P1] Storage encryption-at-rest: StorageService.getPresignedUploadUrl/uploadFile (hmp-shared-packages/packages/infra/src/lib/storage/storage.service.ts:60-109) issues presigned PUTs with NO ServerSideEncryption: 'AES256'|'aws:kms'. Bucket-default SSE is the only line of defense.

Tests. checkin.service.spec.ts (31), checkin.service.p6.spec.ts (9), checkin.controller.spec.ts (8), cform.service.spec.ts (14). No e2e Playwright spec for check-in flow with mandatory-fields rejection; no test asserts the dashboard's data: URL flow round-trips.


Flow 7 — Room Service to Folio​

Verdict: RED (NO-GO). Three blockers.

Data layer.

  • OutletType enum at hmp-pos-service/prisma/schema.prisma:43-53 includes ROOM_SERVICE. OrderType.ROOM_SERVICE exists at :253-259. Outlet uniqueness @@unique([propertyId, code]) (:38).
  • [P0] HSN/SAC codes are not stored on the menu item. MenuItem (schema.prisma:111-139) has taxRate and that's it — no hsnCode, no sacCode, no GST rate-card linkage. The repo CLAUDE.md claims hsnCode exists; it does not. NIC e-invoice IRN requires HSN/SAC per line. CLAUDE.md drift — file is wrong.
  • [P1] OrderItem.modifiers is a Json blob — modifier prices not joined relationally to ItemModifier at order time.

Backend endpoints.

  • POST /orders/:id/room-charge at hmp-pos-service/src/orders/orders.controller.ts:326-345, @Roles SUPER_ADMIN/ADMIN/MANAGER/F_AND_B. Pulls tenantId from JWT; propertyId from header.
  • Validation chain orders.service.ts::postRoomCharge:1141-1318 is solid: tenant-scoped lookup, duplicate prevention via order.folioPosted, cross-service reservation lookup via ReservationServiceClient verifying roomId match AND status==='CHECKED_IN'. Fail-closed on lookup error.
  • [P1] Gateway two-file-rule violation. services.config.ts:405-447 declares /menu, /tables, /kitchen, but proxy.controller.ts only has @All('menu/*'), @All('tables/*'), @All('kitchen/*') (:667, 674, 695) — no bare @All('menu'), @All('tables'), @All('kitchen'). Compare to outlets/orders/shifts/tokens/pre-orders which have both.

Folio-posting integration & idempotency.

  • POS publishes FOLIO_CHARGE_POSTED (orders.service.ts:1306-1315) with v2 typed payload at events/folio-charge-posted.event.ts:15-76. Includes pre-split cgstAmount/sgstAmount/igstAmount/customerState/placeOfSupply and transactionCode mapped from orderType.

  • Receiver PosFolioChargeConsumer (hmp-frontdesk-service/src/folios/consumers/pos-folio-charge.consumer.ts):

    • Feature flag default OFF (:70) — ENABLE_POS_FOLIO_CHARGE_CONSUMER must be 'true'.
    • Dedup key folio:${folioId}:pos-order:${externalOrderId} (:353-366). Test exists at pos-folio-charge.consumer.spec.ts:218.
    • Self-loop guard (:236).
    • Property mismatch guard (:343-348).
    • Single-OPEN-GUEST-folio guard (:296-323).
  • [P0] V2 GST split is dropped by the receiver. extractChargeEvent() at :108-155 extracts only transactionCode/description/amount/taxAmount. The cgstAmount/sgstAmount/igstAmount/customerState/placeOfSupply fields published in v2 are never read (grep on the file: 0 hits). Consumer calls FoliosService.postCharge with no GST breakdown (:373-383). folios.service.ts::postCharge:507-516 re-derives tax from getSACCode(transactionCode) → getGSTRate(...) — losing the customer-state-driven CGST/SGST vs IGST distinction that POS deliberately persisted. CLAUDE.md gotcha #9 documents the rule the code violates. AP-T07-004 is not actually shipped end-to-end.

  • [P1] Dedup-key fallback event.transactionCode || 'FnB' (:375) — legacy hardcoded string the v2 contract was supposed to retire.

Tax handling.

  • POS service charge at orders.service.ts:951-956 correctly splits 50/50 for intra-state, 100% IGST for inter-state. However the rate is hardcoded 0.18 (:140, 807, 952) — does not read property tax config. GST-exempt / composition-scheme properties wrong.
  • MenuItem.taxRate defaults 5 (schema.prisma:121) per-item — no link to property tax-config table.
  • [P1] Tip handling: not implemented. tip/TIP absent from pos-service/src/orders (grep: 0). Order schema has no tip column.

KDS integration. KitchenGateway JWT-verified (hmp-pos-service/src/gateway/kitchen.gateway.ts:101-178). RS256 in production with boot validation. joinOutlet re-verifies outlet.propertyId === client.data.propertyId. Auth is solid. Per CLAUDE.md gotcha #1, OrdersService.generateKOT must fire both WebSocket kot:created AND Kafka pos.kot.created; the doc asserts code does this.

Frontend. Folio page hmp-web-dashboard/src/app/(dashboard)/front-desk/folio/[id]/page.tsx displays transactionCode. No drill-down to originating POS order — FolioTransaction.reference stores folio:<id>:pos-order:<orderId> but UI doesn't parse it.


Flow 8 — Guest Check-out & Invoice​

Verdict: RED (NO-GO for production). Six P0 defects.

Data layer.

  • Folio (hmp-frontdesk-service/prisma/schema.prisma:20-62) carries irn, signedQRCode, ackDate, ackNumber denorm columns. Has closedAt but no settledAt column — settleFolio returns a synthetic settledAt: new Date().toISOString() (folios.service.ts:1450) that is never persisted.
  • Payment.paymentMode enum lacks SPONSORED in DB but schema enum at :145 lists 11 values. No partialAmount on CheckOutDto; split payments not supported via checkout endpoint.
  • EInvoiceJob.ackDate is String? not DateTime — drift from Folio.ackDate DateTime? denorm column.
  • InvoiceSequence composite unique on (tenantId, propertyId, financialYear), atomic lastNumber increment.

Backend endpoints.

  • POST /check-out/:reservationId at checkout.service.ts:51. Acquires 30s Redis lock per reservation. Late-checkout charge auto-posted (:294). Folios closed in batch (:316). E-invoice queued + polled 2.5s (:356-378) — non-blocking on failure.
  • POST /check-out/express (:504) — zero-balance path.
  • POST /late-checkout (:594) — auto-calculates fee with GST.
  • POST /folios/:id/invoice and /folios/:id/invoice/pdf.
  • POST /einvoice/generate, /cancel, /credit-note, /compliance-report.

Settlement modes.

  • [P0] CheckOutDto (dto/checkout.dto.ts:30) accepts a single paymentMode enum and paymentReference. No amount, no split-payment array, no card last4 / cardType. Inline settle loop (:215-267) posts the entire open-folio balance to the single chosen mode. Cash + card combo cannot settle in one call.
  • [P0] paymentMode missing BTC explicitly; CITY_LEDGER is closest but no validation that a companyId is attached when picking it.
  • [P0] Payment.cardLast4/cardType columns exist (schema.prisma:131-132) but inline checkout payment write (:236-247) does not populate them — terminal RRN dropped.
  • [P0] Branded ESC/POS receipt path is gated by ENABLE_FOLIO_PAYMENT_SUCCEEDED_CONSUMER, default OFF (folio-payment-succeeded.consumer.ts:55-61). Inline-settled cash/UPI payments inside the checkout transaction never publish payment.succeeded — receipts only print for terminal-card flows.

GST / IRN compliance.

  • getNextInvoiceNumber (folios.service.ts:1620) resolves prefix from Property.code via PropertyServiceClient, guarded by Redis lock + atomic Prisma increment. Gap-free at row level but no nightly gap-detection cron despite comment at :1629.
  • [P0] Divergent invoice-number generator at invoice-pdf.service.ts:601-618: hard-codes HMP/ prefix, ignores Property.code. Two competing implementations, two output formats.
  • [P0] IRN clobbers invoice number. einvoice-job.processor.ts:64 writes invoiceNumber: result.ackNumber (the 15-digit NIC ack number) after the sequential value was already issued. Once NIC adapter completes, the GST-Rule-46 sequential invoice number is overwritten. Reporting (GSTR-1) loses the per-FY sequence.
  • [P0] B2B vs B2C resolution at einvoice.service.ts:99-101 keys off folio.company?.gstin. Walk-in guests with personal GSTIN can never become B2B — no Folio.guestGstin field. AATO ₹5cr threshold not validated anywhere — every B2B folio is pushed for IRN regardless.
  • [P0] Inter-state vs intra-state derived from GSTIN first-2-digits (:103-110) and falls back to propertyStateCode when guest address is unavailable — guest read-model carries no addresses relation. Every B2C folio defaults to intra-state CGST/SGST, even if the guest is from a different state — IGST should be the correct treatment.
  • [P0] No production guard against EINVOICE_MOCK=true. Mock-IRN path generates SHA-256-based 64-char IRNs that are structurally valid but not registered with NIC. A misconfigured prod deploy that inherits EINVOICE_MOCK=true will issue invoices that look real.
  • [P1] NIC adapter has only 2 specs in nic-einvoice.adapter.spec.ts — auth flow + happy-path. No retry-on-401, no idempotency-key, no rate-limit-window, no production cert pinning.

Receipt / PDF. generateFolioReceipt in hmp-shared-packages/packages/common/src/lib/printing/receipt-templates.ts. Spec has 53 cases (above the claimed 47) covering ESC/POS bytes, GST splits, HSN, IRN. Solid. "DRAFT — IRN PENDING" watermark correctly emitted by invoice-pdf.service.ts:570-580 when folio.irn is null.

Cross-service & events.

  • [P0] No invoice attachment on thank-you email. DispatcherService.handleEvent('reservation_checked_out') (hmp-notification-service/src/dispatcher/dispatcher.service.ts:130-132) routes to generic HTML, EMAIL only, no folio invoice attachment. There is no folio.closed or folio.settled event published.
  • [P0] No FOLIO_PAYMENT_RECEIVED publish from inline checkout payments (checkout.service.ts:236). Reservation-service FolioPaymentConsumer (added 2026-05-05) is bypassed → paymentStatus drift.
  • [P1] Duplicate task creation. Housekeeping EventsService.handleCheckoutEvent (hmp-housekeeping-service/src/events/events.service.ts:155) creates a CHECKOUT_CLEANING task on RESERVATION_CHECKED_OUT, AND the explicit HOUSEKEEPING_TASK_CREATED event from frontdesk has no consumer-side dedup. Two HK tasks per checkout.
  • No loyalty accrual event on checkout — spec asks for accrual; no code path here calls a loyalty service.

Tests. checkout.service.spec.ts (24), folios.service.spec.ts (25), einvoice.service.spec.ts (31), invoice-pdf.service.spec.ts (10), nic-einvoice.adapter.spec.ts (2), receipt-templates.spec.ts (53). invoice-numbering.spec.ts (4) tests a local helper, not the production path. No concurrency test for invoice numbering, no gap-detection test.


Flow 9 — Night Audit​

Verdict: RED (NO-GO for property #1). Eight P0 issues.

Data layer. NightAuditLog (hmp-frontdesk-service/prisma/schema.prisma:346-367) — status RUNNING/COMPLETED/COMPLETED_WITH_ERRORS/FAILED/ROLLED_BACK, KPIs, errors Json, performedBy.

  • [P0] NO unique constraint on (propertyId, auditDate) in frontdesk's schema; only @@index. Duplicate-run defense is a findFirst race window in night-audit.service.ts:227-241. Redis lock (audit:lock:{propertyId} 30-min TTL, :130-150) closes most of that window in practice, but with Redis unavailable for audit lock — proceeding without lock (:147) the system openly downgrades to no-lock — a single Redis blip during cron can double-post a property's nightly charges. Analytics-service mirror DOES have it (hmp-analytics-service/prisma/schema.prisma:202) — read-side safer than write-side.

Trigger & idempotency.

  • @Cron(CronExpression.EVERY_HOUR) in night-audit-scheduler.service.ts:17, computes localHour per property timezone (Asia/Kolkata default), runs only when localHour === settings.nightAuditHour ?? 2. Wraps in DistributedLockService.withLock.
  • Manual POST /night-audit/:propertyId/start (night-audit.controller.ts:132-153), ?dryRun=true short-circuits.
  • Chain audit POST /night-audit/chain runs sequentially across tenant properties.
  • Idempotency: per-folio room-charge dedup checks transactionCode='ROOM' AND postingDate∈[today,tomorrow] AND isVoid=false (:456-474).
  • [P0] Dedup window timezone math wrong. :454: todayStart.setUTCHours(0,0,0,0) on a today value computed from property local-midnight (:225). For a property in IST, today is 2026-05-05T18:30:00Z; todayStart becomes 2026-05-05T00:00:00Z, sliding the dedup window 5.5h backwards. Charges posted at 23:00 IST on 5 May (17:30 UTC, falling into UTC 5 May) accidentally land in scope.

Posting loop.

  • :392-410 cursor-paginates status=CHECKED_IN reservations (200/batch).
  • [P0] Rate computed by averaging the booking's total over the stay, not by reading today's RatePlanRate. Any rate change mid-stay invisible — guest billed the original average. Real PMS posts the night's contracted rate from the rate calendar.
  • No CGST/SGST/IGST split happens here — taxAmount is a single number; folio.update :542-552 only increments totalTax.
  • Overstay tagging (:484-537): when reservation.departureDate < todayMidnight code tags OVERSTAY_NIGHT — correct.
  • createMany + parallel folio updates is sound for ≤1000 rooms but holds a single 120s interactive transaction (:665) that can blow past the timeout on 500+ rooms.
  • [P0] Multi-currency aggregation fudged: :418-427 logs WARN if any reservation has non-INR currency, proceeds with 1:1 sums anyway. Total revenue, ADR, RevPAR, GSTR-3B all wrong on USD/AED bookings.

No-show. :559-655 posts first-night cancellation fee. Updates reservation status=NO_SHOW.

  • [P0] NO consultation of CancellationPolicy. Some plans charge 100% of stay, some 1 night, some non-refundable already deposited. Posting "first night" universally violates contract terms on prepaid rate plans. No check whether deposit already collected — risk of double-charging.

Reports.

  • night-audit-pdf.service.ts uses pdfkit; GET /night-audit/record/:id/pdf returns PDF, @Roles SUPER_ADMIN/ADMIN/MANAGER/ACCOUNTS.
  • [P0] PDF generated on-demand, NOT stored to S3 or any durable store. No retention policy. No archival. No CSV export endpoint. RBI/CERT-In require 5-year audit trail; this PDF is render-and-discard.
  • Property name on PDF is propertyName? parameter the controller never passes (:86) — every PDF says NIGHT AUDIT REPORT with no hotel identification.
  • Email template at hmp-notification-service/src/templates/night-audit-report.ts dispatched on night.audit.completed Kafka event — emails the GM/Revenue Manager. The only durable distribution.

Cross-service.

  • 6 bus events. night.audit.completed (:295-304) consumed by NightAuditConsumer (analytics) which upserts MvDailyOccupancy/MvDailyKpi/NightAuditLog. Also notification dispatcher.
  • [P0] reservation.no_show per-reservation (:695-702) — fire-and-forget with .catch() log only, NOT in-transaction. DB write committed before events published. On Redis Streams broker outage right after commit, no-shows go silent — analytics MV stale, channel-service never sends OTA cancellation.
  • POS reconciliation: :758-806 calls PosServiceClient.getPosRevenueTotal, raises NA_POS_RECONCILIATION_MISMATCH safeguard if variance > ₹1.
  • [P0] NO POS day-close cascade. Audit treats POS as passive read-source. hmp-pos-service has shifts/ (cash) but no closeOutletForDay endpoint. Open POS order at 02:00 IST (1) not in revenue total, (2) post-audit charges flow to same folio under new business date, (3) reconcile silently undercounting.

Frontend. hmp-web-dashboard/src/app/(dashboard)/night-audit/keynova.tsx:1-120. Pre-checks call getNightAuditPreCheckReport. Has rollback button.

  • [P1] validatePreAudit silently catches all cross-service errors and reports 0 mismatches: :1057-1062 swallow PropertyServiceClient outage. Dashboard says "All clear, proceed" while data is unreachable. canProceed (:1275) derived purely from pendingDepartures === 0 — open folios, dirty rooms, FO/HK mismatches do NOT block.

Tests. night-audit.service.spec.ts (55), chain-audit-orchestrator.spec.ts (17), night-audit.controller.spec.ts (16). Dual-run idempotency test exists but no concurrent-goroutine simulation. No tests for: timezone-shifted dedup window, multi-currency drift, cancellation-policy-aware no-show, POS-open-shift cascade, failed validatePreAudit masking.


Flow 10 — Reports & Dashboards​

Verdict: RED (NOT launch-ready for property #1).

Required reports inventory.

KPI / ReportMVDefinition fileStatus
Occupancy %MvDailyOccupancy (hmp-analytics-service/prisma/schema.prisma:36)occupancy.definition.ts (occupancy-report)OK
ADR / RevPARMvDailyKpi (schema.prisma:161)revenue-daily.definition.ts:18; manager-flash.definition.ts:27OK
Arrivals todayMvDailyArrivals (:308)arrival.definition.ts (arrival-report)OK
Departures todayMvDailyDepartures (:328)departure.definition.ts (departure-report)OK
In-houselive-proxy (not MV)in-house.definition.ts (in-house-report)Live-proxy
Daily revenue (room)MvDailyKpi.totalRoomRevenuerevenue-daily.definition.tsOK — only roomRev
Top room typesNONE — falls back to EventStore.findManyroom-type-revenue.definition.ts:37BROKEN — bypasses MV pattern
Channel mixMvChannelPerformancesource-wise-revenue.definition.ts:50OK
Source mixSamesameOK
YoY vs priorMvDailyKpi LY window via LyEnrichercomparator/ly-enricher.ts:83PARTIAL

MV consumer wiring. EventDispatcherService (src/consumers/event-dispatcher.service.ts:39-75). 16 consumers wired. SKIP_EVENT_SUBSCRIPTIONS=true skip flag. ReservationConsumer subscribes 10 topics, upserts 9 MVs. NightAuditConsumer (night-audit.consumer.ts:82-226) authoritative SET.

API endpoints. GET /api/reports/v2/catalog, POST /api/reports/v2/:id/run?format=..., GET /api/reports/v2/:id/runs. Dashboard endpoints at dashboard.controller.ts:98-238. Gateway routing wired.

  • [P2] night-audit-reports registered at proxy.controller.ts:1429 but I don't see a matching services.config.ts entry — possible 404 trap (two-file rule).

Frontend. Legacy /reports redirect IS in place: next.config.ts:64-66 emits 308 to /reports/v2 (with regex (?!v2$) to prevent redirect-loop). v2 catalog at (dashboard)/reports/v2/page.tsx. Run page at (dashboard)/reports/v2/[reportId]/page.tsx. Anomalies page calls real endpoint — confirmed not stubbed. Operations Hub home gates on isOperationsHubEnabled() (defaults ON post-2026-05-02). ActionsQueueSection calls /actions endpoint that does not exist yet — endpoint returns 404, hook silently empty-states.

KPI-tile broken-IDs (showstopper).

  • [P0] tiles/kpi/revpar.tile.tsx:11,19, adr.tile.tsx:11,19, occupancy.tile.tsx:11,19 reference reportId="manager-flash". Registered ID is manager-flash-report.
  • [P0] arrivals-today.tile.tsx:11,18 uses arrival vs registered arrival-report.
  • [P0] departures-today.tile.tsx uses departure vs departure-report.
  • [P0] inhouse-count.tile.tsx uses in-house vs in-house-report.
  • [P0] no-shows-today.tile.tsx uses no-show-reservation vs no-show-reservation-report.
  • Every Operations Hub home tile will hit the registry, miss, and surface an error/empty card. KPI-card test file (kpi-card.test.tsx:22,31,…) propagates the same wrong ID — bug never caught.

Numerical-correctness deep-dive — Daily Occupancy / ADR / RevPAR.

Three concrete bugs in the math:

  • [P0] roomsSold increments by 1 per reservation.created (reservation.consumer.ts:175,200). Counts bookings, not room-nights. A 3-night reservation contributes roomsSold=1 and totalRoomRevenue=full 3-night total, so adr = 3-night total / 1 = 3× the real ADR. Night audit later overwrites this (:179: roomsSold = occupiedRooms), but until audit runs the dashboard shows wildly inflated ADR.
  • [P1] totalRooms keyed by propertyId_date on MvDailyOccupancy but seeded from 0 on first reservation.created of the day if no occupancy row exists yet (reservation.consumer.ts:170-173). Result: revpar = 0 until first reservation.checked_in (which doesn't write totalRooms either). RevPAR shows 0 every fresh day until ~02:00 IST.
  • [P0] Date is new Date(arrivalDate) (reservation.consumer.ts:103). If arrivalDate is date-only "2026-05-06", new Date('2026-05-06') = UTC midnight. Same in night-audit.consumer.ts:121. Dashboard's getTodayAdrRevparRow (dashboard.service.ts:255) builds today in server local time, NOT property TZ. Run on UTC pod and "today" can disagree by 5h30m from "today IST" between 18:30-23:59 IST. Asia/Kolkata not honored on any MV write or any dashboard read.

Date filters / timezone. resolvePeriodBounds (reporting/reports/shared/period-helpers.ts:9-40) hardcodes UTC for today / yesterday / mtd / ytd / wtd. After 18:30 UTC each day the IST date has rolled over; reports run with period: { type: 'today' } between 18:30 IST and midnight IST will return rows for the previous IST day.

Saved views & exports. ReportSavedView Prisma model exists. Endpoints registered. Default schedule timezone IS Asia/Kolkata (saved-views.controller.ts:131) — good. Export pipeline 5 formatters; sync ≤500 rows; queued via BullMQ above. PDF requires Chromium — SKIP_PUPPETEER must be set in CI/test or render() throws.


Flow 11 — ResAvenue Channel Manager​

Verdict: AMBER. Cert-handshake plausibly complete (real partner traffic verified); 4 P0 issues remain in the production code path.

Data layer. IntermediaryConnection (hmp-channel-service/prisma/schema.prisma:496-532) — encrypted creds, webhookCallbackUrl, webhookSecret, outboundIp. IntermediaryProviderConfig (schema.prisma:626-) — Tier-1 platform-tier provider catalog.

  • [P0] WebhookEvent (schema.prisma:317-355) has NO @@unique constraint on (provider, parsedBookingId, status) or (provider, parsedBookingId, eventType). Idempotency relies on findFirst + manual status='ACCEPTED' check (resavenue-webhook.controller.ts:480-488, :758-765) — race-vulnerable on concurrent retries.
  • ChannelSyncLog gained requestXml/responseXml columns 2026-05-05; SyncService persists actual wire bytes.

Push primitives.

  • Rates: POST /channel-sync/push-rates. Redis setNx distributed lock (60s), 3-attempt exponential backoff (sync.service.ts:130-156). ResAvenueXmlBuilder.buildRateXml (resavenue-xml.builder.ts:238-309) emits OTA_HotelRateAmountNotifRQ with <BaseByGuestAmt NumberOfGuests=1/2>, <MinStay>/<MaxStay>/<StopSell> inside <Rate>, optional <Promotion>, optional <OTAName> (omitted by default per spec v1.7).
  • Inventory: POST /channel-sync/push-availability. buildInventoryXml (:171-217) emits OTA_HotelInvCountNotifRQ with StatusApplicationControl Start/End/InvTypeCode + InvCounts (with optional weekday attrs Mon...Sun), <InvCount Count>, <StopSell>, <CloseOnArrival>, <CloseOnDeparture>, <CutOff>. AP-T11-013 drip-feed allocation cap applied before adapter call.
  • Stop-sale: rate path (<StopSell> inside <Rate>, since 2026-05-05 spec v1.7 page 11) and inventory path. KNV_R_07 cert test exercises rate path.
  • Restrictions: POST /channel-sync/push-restrictions. ResAvenueAdapter.handleRestrictionPush (resavenue.adapter.ts:59-118) splits restrictions: closure flags ride inventory XML; stay-only rows silently dropped with console.warn only — caller must re-route via pushRates. Counted as droppedStayOnly but never surfaced in SyncResult.errors[] (:84-89).
  • Range collapse: verified working. collapseRanges (:91-128) groups strictly-consecutive same-value rows. Cert evidence KNV_I_02 collapsed 30 days into one block.

Inbound webhook & parser.

  • POST /webhooks/resavenue/booking (resavenue-webhook.controller.ts:227-262) — root-element dispatcher.
  • Reservation flow (processReservation:323-601): IP allowlist, credential check, parse via ResAvenueXmlParser, multi-RoomStay aggregation (:507-512), idempotency via findFirst + status check, normalize via ChannelBookingNormalizer, publish CHANNEL_BOOKING_RECEIVED Kafka topic.
  • HMAC-SHA256 with timingSafeEqual (:103-112).

Failure handling & cert pinning.

  • 3-attempt exponential backoff (100/200/400ms).
  • 30s fetch timeout via AbortController (resavenue.adapter.ts:121-145).
  • [P1] Cert pinning agent factory exists (pinning-https-agent.factory.ts) but is NOT wired into ResAvenueAdapter.sendRequest which uses plain fetch() (resavenue.adapter.ts:125-130). CLAUDE.md acknowledges this gap. Live ResAvenue endpoint is HTTP today (http://203.109.97.241:8080/...) so pinning is moot until they migrate to HTTPS.

Tests & cert evidence. 6 ResAvenue-specific spec files totaling 2,691 lines. Cert format spec has 13 byte-level shape assertions. Cert evidence file at hmp-docs/Integrations/ResAvenue/cert-evidence-2026-05-05.md documents 18 PASS rows (KNV_I_01..09 + KNV_R_01..09) against HotelCode 261 with real <Success/> responses captured in XML. Memory's claim is accurate.

Frontend. Sync logs / webhook events: /distribution/audit?type=outbound|inbound. Mapping CRUD: /settings/channels/mappings, /managers, /routes. Admin portal: channel-providers, connections-overview, changes (4-eyes queue).

P0 issues blocking launch.

  1. Webhook idempotency race-vulnerable. Add @@unique([provider, parsedBookingId, eventType]) partial index OR Redis SETNX with the booking ID. Two concurrent ResAvenue retries could both pass the findFirst and double-publish.
  2. itemsProcessed hardcoded to 1. ResAvenueXmlParser.parseSyncResponse returns itemsProcessed: 1 regardless of how many rows were in the request (resavenue-xml.parser.ts:27,31). 360-rate push reads as itemsProcessed=1. Sync metrics + dashboards are wrong.
  3. Stay-only restrictions silently dropped. handleRestrictionPush warns to console only (:84-89); SyncResult.errors[] stays empty — API responds 200 OK while nothing happens.
  4. No DB-level idempotency on outbound retries. Three attempts in 100/200/400ms — if ResAvenue returns 200 with <Errors>, retry resends the same XML. No idempotency key on outbound path.

Flow 12 — POS Terminal & Payment Gateway​

Verdict: AMBER. Gateway path is launch-ready; card-present has reconciliation gap; subscription docs lie.

Data layer. hmp-payment-service/prisma/schema.prisma is real and substantive. ProviderCatalog (:148), MerchantAccount (:164, AES-256-GCM envelope encrypted, dekWrapped, kmsKeyVersion, no plaintext credentials), Payment (:198, idempotency key unique per tenant :226, status machine via PaymentStatus :40), PaymentAttempt (:232, carries only cardLast4 :242 + cardBrand :243 — no PAN), Refund :264, PaymentLink :284 with tipAmount :320, PaymentLinkBatch :337, TerminalDevice :354 (with OutletKind :366 covering FRONT_DESK/POS_OUTLET/BANQUET_VENUE/SPA_OUTLET), UpiQrCode :389, WebhookEvent :418 (@@unique([providerType, providerEventId]) :438 — DB-level idempotency), Subscription + SubscriptionCharge :463/498. Transactional outbox PaymentOutbox :545 with FOR UPDATE SKIP LOCKED claim semantics — production-grade.

POS terminal (card-present). PineLabs adapter is real (adapters/pinelabs/pinelabs-terminal.adapter.ts:60-279) — TCP socket against Plutus ECR, length-prefixed JSON frames. Mock/sandbox/live mode handling. ECONNREFUSED/EHOSTUNREACH → ProviderDeviceOfflineError. Tip-amount path real (:137, plutus-ecr.codec.spec.ts:37-46). Razorpay POS terminal adapter at razorpay-terminal.adapter.ts — poll-until-captured. REST surface TerminalChargeController at terminals/terminal-charge.controller.ts:43-74, returns 202, async dispatch via TerminalChargeWorker. Roles ADMIN | SUPER_ADMIN | FRONTDESK_AGENT | POS_CASHIER. Two-file rule satisfied for /terminal-devices (services.config.ts:525-530, proxy.controller.ts:999/1006).

  • [P0] No HMP-side terminal batch-settlement reconciliation. PineLabs batchSettlement at pinelabs-terminal.adapter.ts:246-252 is documented no-op: "Batch settlement is handled on-device by Pine Labs." Dashboard POS hardware page calls /pos/hardware/batch-settlements against pos-service which has no batch-settlement source-of-truth. End-of-day reconciliation between terminal acquirer batch and HMP Payment ledger is not implemented for PineLabs. RED for any property using card-present.

Razorpay online flow. Real adapter (razorpay-gateway.adapter.ts:34-279). Keys read from MerchantContext credentials (encrypted at rest, decrypted via CredentialCipherService); zero hard-coded keys in repo. Order creation passes payment_capture: 1 (:85), X-Razorpay-Idempotency-Key header (:93). verifyAndParseWebhook :152-177 throws ProviderSignatureError on mismatch. Status mapper covers authorized → AUTHORIZED, captured → CAPTURED, failed → FAILED, refunded → REFUNDED.

  • [P1] No SETTLED or CHARGEBACK state in PaymentStatus enum — gap for dispute / settlement reporting.

Webhook & idempotency. WebhookController at webhooks/webhook.controller.ts:31-71 is @Public(), accepts :providerCode/:merchantAccountId, raw-body via body-parser.raw registered before JSON parser. HMAC verified by verifyRazorpayWebhookSignature (razorpay-signature.ts:11-19) using createHmac('sha256').digest('hex') and timingSafeEqual — proper constant-time. Layered idempotency: tentative dedup, authoritative dedup, P2002 race recovery, DB-level unique constraint, failed-signature audit row. Reconcile cron every 10 min, capped 5 attempts. Production-grade.

Refunds. PaymentOrchestratorService.refund :330-540. Validates payment SUCCEEDED/PARTIALLY_REFUNDED. Computes priorTotal, enforces requested ≤ remaining. Idempotency via metadata path-match (:384-400).

  • [P1] Refund idempotency lives in metadata.idempotencyKey JSON path — should be a first-class indexed column.
  • Routes to terminal vs gateway based on attempt.terminalDeviceId :415. Provider failure marks refund FAILED. Webhook flips final status (webhook-dispatcher.service.ts:528-602). Audit row + payment.refunded event published.

Subscriptions.

  • [P0] SUBSCRIPTION_VIA_PAYMENT_SERVICE_ENABLED flag does NOT exist in code. hmp-auth-service/src/subscriptions/subscription.service.ts injects PaymentServiceClient unconditionally (:8, :69). Legacy adapter + flag have been REMOVED (subscription.module.ts:12: "RazorpaySubscriptionAdapter and its feature flag have been removed"). However, hmp-auth-service/src/billing/subscription.service.ts:189,370,890,926 and billing/billing.service.ts:207-481 still hold private createRazorpaySubscription/cancelRazorpaySubscription methods — DEAD code path. Root CLAUDE.md is stale on this flag.

PCI. PCI-redaction infrastructure real at hmp-shared-packages/packages/common/src/lib/sanitizer/pci-sanitizer.ts:2-3 (CARD_FIELDS, CVV_FIELDS) with Luhn validation, masks PAN to ****1234, redacts CVV to ***. payment-orchestrator.service.ts:640 actively scrubs metadata via regex. Schema persists only cardLast4+cardBrand (:242-243); no PAN/CVV/track data anywhere. Booking-engine card form at hmp-booking-engine/src/pages/direction-a/CheckoutPage.tsx:279,281 uses useState('4532 •••• •••• 8842') — hardcoded display placeholders, never user input. Real flow uses Razorpay hosted checkout via useRazorpayCheckout (PaymentForm.tsx:5,48). SAQ-A scope preserved. Key-cards page uses cardNumber: string for room key card identifier — confirmed safe.

Frontend. Dashboard: front-desk/folio/[id]/page.tsx:1271 mounts TerminalChargeModal. POS hardware page calls /pos/hardware/batch-settlements — endpoint not actually backed by payment-service. Booking-engine: real Razorpay widget wired.

  • [P0] Refund modal absent from dashboard. Backend endpoint exists; no UI. Frontdesk-manager users can't trigger partial refunds without API call.

Tests. Real coverage: razorpay-signature.spec.ts (4), webhook-dispatcher.service.spec.ts (11), payment-orchestrator.service.spec.ts (28), razorpay-gateway.adapter.spec.ts (idempotency-header tests), razorpay-terminal.adapter.spec.ts, pinelabs-terminal.adapter.spec.ts, plutus-ecr.codec.spec.ts, terminal-charge.service.spec.ts, terminal-charge.worker.spec.ts, pci-log-verification.spec.ts:33,124.

P0 hard blockers.

  1. Bug — razorpay-terminal.adapter.ts:184,293 still send X-Payment-Idempotency-Key (silently ignored by Razorpay, defeats POS-terminal retry idempotency). The fix from AP-T12-003 was applied to gateway adapter but missed for terminal. Two-line fix.
  2. No HMP-side terminal batch-settlement reconciliation.
  3. Refund modal absent from dashboard.
  4. SUBSCRIPTION_VIA_PAYMENT_SERVICE_ENABLED documented but does not exist.

Flow 13 — Tape Chart Calendar​

Verdict: RED. This is the operational heart of any PMS, designated by the user as the hero feature, and the production shell is a half-implemented makeover that has dropped four of the eight checklist operations the older code already supported.

Page & component existence. Tape chart at /front-desk/calendar. Page file is a re-export: (dashboard)/front-desk/calendar/page.tsx:1 → ./keynova. keynova.tsx:8,334 mounts <TapeChartV2Shell /> from @/components/modules/front-desk/tape-chart-v2. Two tape-chart codebases in repo: older tape-chart/ (with virtualization, context menus, bulk-select toolbar, BAR_ACTIONS, CELL_ACTIONS) and newer tape-chart-v2/ (what production loads). The v2 shell is missing most of v1's features. Central finding.

Grid rendering & virtualization. tape-chart-v2/chart-grid.tsx:143-265 renders the entire grid as one CSS-grid: gridTemplateColumns: 'var(--room-col-w) repeat(var(--total-days, 14), var(--col-w))' (:155) with all rooms inline. No virtualization. The codebase ships a working @tanstack/react-virtual layer (tape-chart/virtual/use-virtual-chart.ts:5,80, virtual-row-renderer.tsx), but tape-chart-v2-shell.tsx does not use it. Grep for Virtual|tanstack/react-virtual|react-window under tape-chart-v2/: zero references.

Backend bulk endpoint. GET /front-desk/calendar?from=&to=&roomTypeId=&floor=&source=&vipOnly= — single bulk fetch.

  • Controller: hmp-frontdesk-service/src/calendar/calendar.controller.ts:18-49.
  • Service: calendar.service.ts:94-438. Tenant-asserted (:105: propertyClient.assertPropertyTenant), max 90-day window enforced (:117). Returns {rooms, roomTypes, reservationBlocks, blockedRooms, unassignedReservations, availability, roomBlocks}.
  • Not paginated, not streamed. For 100 rooms × 30 days × ~40 active reservations, payload is acceptable. For 500 rooms × 90 days the unbounded roomReservation.findMany (:167) will hurt — no per-page cap.
  • Frontend: use-tape-chart-data.ts:46-82 with staleTime: 30s, no polling (WebSocket-driven via useRealtimeTapeChart).

Drag-move + extend wiring. Drag implemented via @dnd-kit. The handlers are real and wired:

  • tape-chart/drag/use-drag-reservation.ts:39-123. kind === 'resize-right' → extend; kind === 'move' → reassign. kind === 'resize-left' explicitly out of scope ("Phase 2", :81-83) — shorten-by-dragging-left-edge does not work.
  • Extend mutation: use-tape-chart-actions.ts:286-291 → PUT /reservations/:id { departureDate }.
  • Move (CHECKED_IN): :347-354 → POST /room-move.
  • Move (pre-arrival): :356-359 → POST /reservations/assign-room. Comment at :622-625 admits the assign endpoint takes ~30s in dev.
  • Optimistic cache patch + revert + undo-toast pipeline solid (runCommand :194-250).

Context menu actions.

  • [P0] v2 does not wire context menus. BAR_ACTIONS (check-in, check-out, move-room, extend, shorten, add-charge, view-folio, print-grc, mark-no-show, cancel) and CELL_ACTIONS defined in tape-chart/context-menu/context-menu-items.ts:71-95. BarContextMenu wired in v1 tape-chart/room-row.tsx:240-259. tape-chart-v2/bar.tsx:83,180 accepts onContextMenu; chart-grid.tsx:27,254 accepts onBarContextMenu. But tape-chart-v2-shell.tsx (:339-357) never passes either — grep returns zero. Right-clicking a bar in production does nothing. No CellContextMenu either.
  • [P0] DetailPanel actions are placeholders. tape-chart-v2-shell.tsx:365-372 — every action except open-reservation shows toast.info('${action} for ${res.confirmationNumber} — coming in Phase 2'). So even from the side panel, check-in / check-out / cancel / room-move are non-functional.

Conflict & concurrency. Drag-time overlap check (use-drag-reservation.ts:55-67, :97-105) — client-side scan, blocks drop with toast. Server-side: use-tape-chart-actions.ts:239-247 — on 409/404, optimistic patch reverted and calendar query invalidated.

  • [P1] No version/ETag/If-Match column on Reservation. Grep for version|@@version|ETag|If-Match in reservation schema: none. "409" handling is purely a generic-toast fallback; if two staff drag the same bar simultaneously, the second write silently wins on the database side.

Performance. 100 rooms × 30 days = 3,000 cells: realistic with virtualization. Without it, tape-chart-v2/chart-grid.tsx mounts:

  • 100 sticky room-name divs + ~5 group-row divs.
  • 100 × 30 = 3,000 <RoomRow> cell divs.
  • N reservation bars (<Bar>) flat-mapped at :238-258.
  • With active drag, CSS-grid recompute on resize, and a non-memoized allReservations flatten at :136-141, real-world 100×30 will land in 400ms–1s, getting worse linearly past that. AMBER-to-RED.

Mobile & a11y.

  • [P1] Mobile: zero handling. No useMediaQuery, no matchMedia, no breakpoint guard, no "desktop only" message under tape-chart-v2/. Pinch-zoom drag conflicts with @dnd-kit.
  • [P1] A11y: Bar has role="button" (bar.tsx:175). No aria-label on day-headers, no aria-rowcount/aria-colcount, no keyboard cell focus model, no live-region announcements. useKeyboardShortcuts exists for pan/zoom only — onOpenDetail/onOpenWalkIn callbacks (:190-191) are () => {} stubs.

Tests.

  • [P0] Unit tests for v2: zero. Search tape-chart-v2/ for .test.|.spec.: nothing.
  • [P0] E2E: only 2 specs, 5 tests total — e2e/tape-chart-phase2.spec.ts (renders without errors, keyboard Home doesn't crash) and tape-chart-phase3.spec.ts (3 similar smokes). No drag-move e2e, no extend e2e, no walk-in e2e, no context-menu e2e, no conflict e2e.

Remediation ranked by ROI.

  1. Wire BarContextMenu and CellContextMenu into tape-chart-v2-shell.tsx — components already exist. One day's work to restore the missing 50% of operations.
  2. Replace v2's chart-grid.tsx:143-268 body with v1 virtualized renderer. One day's work using existing code.
  3. Replace 'coming in Phase 2' placeholder in tape-chart-v2-shell.tsx:365-372 with real handler dispatch. Half a day.
  4. Add version: Int @default(0) column to Reservation, increment on every update, send as If-Match header from drag handlers. ~1 sprint.
  5. Restore BulkActionToolbar and Selection wiring in v2; expand to OOO range-block.
  6. Add Playwright e2e for drag-move, drag-extend, walk-in click, context-menu cancel, conflict reject. Hero feature deserves a 20-spec suite minimum.

The fastest path to GREEN is not new code — it is plumbing the existing v1 logic into the v2 shell. Most of the missing features are already written and tested.


§4 — Cross-cutting findings​

4.1 Build hygiene — silent compilation failures​

  • [P0] Every backend service's build script is tsc -p tsconfig.docker.json || true. Confirmed for hmp-property-service, hmp-reservation-service, hmp-frontdesk-service (and the pattern is consistent across the platform). The || true swallows TypeScript compilation errors. A service with a real type error ships the previous dist/ (or empty) and the Docker build still succeeds. The blast radius is unknown.

4.2 xPrisma anti-pattern — persistent and growing​

  • [P0] 79 foreign-model Prisma access sites in channel-service + reservation-service alone (excluding spec files). Examples:
    • hmp-channel-service/src/intermediary/channel-booking-normalizer.service.ts — prisma.roomType.findFirst, prisma.ratePlan.findFirst.
    • hmp-channel-service/src/sync/sync.service.ts — prisma.roomType.findMany.
    • hmp-reservation-service/src/booking-engine/booking-engine.service.ts — prisma.property.findUnique, prisma.guest.findFirst (multiple call sites).
    • hmp-reservation-service/src/internal/tenant-export.service.ts — prisma.property.findMany.
  • These services ship local read-models (e.g., reservation-service's Property/Guest models, channel-service's RoomType/RatePlan) maintained by read-model-sync.service.ts consumers. The platform CLAUDE.md asserts the canonical pattern is @hmp/service-clients; the read-model-sync consumers are a parallel implementation. Drift between the canonical owner (property-service / guest-service) and the consumer's read-model is silent and only catchable by integration test.
  • Confirmed by Flow 4 sub-agent: dashboard reservation detail page reads reservation.guest.email|phone|totalStays|vipLevel directly from this read-model; if the sync consumer lags, staff see stale guest contact info.

4.3 Two-file rule violations​

  • [P0] /extra-charges registered in hmp-api-gateway/src/config/services.config.ts:1055 but missing entirely from proxy.controller.ts.
  • [P0] /seasons missing from BOTH files.
  • [P1] /menu, /tables, /kitchen have config entries but only */* controller decorators (no bare @All('menu')/etc.). Sub-routes work; bare root paths 404.
  • [P2] /auth has @All('auth/*') but no bare @All('auth'). Inconsistent.
  • [P2] night-audit-reports has decorator at proxy.controller.ts:1429 but no services.config.ts entry.

4.4 Idempotency gaps on critical paths​

PathStatusFile
Booking-engine confirmMISSING at inventory boundarybooking-engine.service.ts:981-1068
Razorpay capture (gateway)OK (X-Razorpay-Idempotency-Key)razorpay-gateway.adapter.ts:93
Razorpay capture (terminal)BUG — sends defunct keyrazorpay-terminal.adapter.ts:184, 293
Webhook receive (payment-service)OK (@@unique([providerType, providerEventId]))prisma/schema.prisma:438
OTA inbound (channel)race-vulnerable findFirstresavenue-webhook.controller.ts:480-488
Check-inOK (Reservation.status guard)checkin.service.ts:183-234
Check-outOK (Redis lock 30s)checkout.service.ts:54
Night auditNO DB unique on (propertyId, auditDate)prisma/schema.prisma:346-367
POS folio postOK (dedup-key in FolioTransaction.reference)pos-folio-charge.consumer.ts:353-366
Tenant signupOK (Tenant.code @unique)prisma/schema.prisma:22

4.5 Event-subscription wiring (compose flags)​

SKIP_EVENT_SUBSCRIPTIONS discipline:

  • dev-all.yml: defaults true, explicitly =false on consumer services (property, hk, frontdesk, channel, pos, analytics, notification). Correct.
  • prod-aws.yml:64: one service has SKIP_EVENT_SUBSCRIPTIONS: "true" — verify which (likely api-gateway, which is correct; if it's a consumer, P0).
  • qa-aws.yml:113: same =true on at least one service.
  • dev-all.yml:32: JWT_PUBLIC_KEY PEM hard-coded in master. Even if dev-only, an actual RSA public key sits in repo history forever.

4.6 Compliance posture (Indian regulatory)​

RegulationStatusEvidence
GST CGST/SGST/IGST splitPARTIAL — schema persists at POS event source, but receiver drops it (Flow 7 P0); B2C falls through to intra-state (Flow 8 P0); slab boundary hard-coded (Flow 2 P0).
GST e-invoice IRN (NIC IRP)MOCK ONLY in QA; no production guard against EINVOICE_MOCK=true (Flow 8 P0); ack number overwrites sequential invoice number (Flow 8 P0); 2 specs total on real adapter.
Form C / FRRONON-FUNCTIONAL — empty-nationality bypasses (Flow 6 P0); walk-in skips entirely (Flow 6 P0); arrivedFrom/visaType dropped during PDF generation (Flow 6 P0); FRRO submission is ManualUploadFrroClient (no real e-FRRO API).
Aadhaar / UIDAIOK for guest-service storage (encrypted, last-4 mask). AMBER for frontdesk-service GuestIdVerification — falls back to plaintext on missing ENCRYPTION_KEY (checkin.service.ts:91-95).
State guest-register (police register)NOT IMPLEMENTED — reporting-feeds.service.ts:458 is a stub returning null PII (Flow 6 P0).
DPDPA / data residencyNOT IMPLEMENTED — no dataResidency column on Tenant; no RLS middleware (Flow 1 P0).
CERT-In incident reportingwiring exists in auth-service /incidents but no integration with tenant-lifecycle / payment / data-breach surfaces (Flow 1 P1).
TRAI DND for SMSnot verified; out of scope of 13 flows.
PCI DSSOK — booking-engine uses Razorpay hosted checkout (SAQ-A), payment-service stores only cardLast4+cardBrand, no PAN/CVV anywhere in repo (Flow 12).

4.7 Schema drift​

  • tenantId String @db.Uuid consistent across all 16 sampled services. PASS.
  • Reservation-service Prisma carries 21 models including foreign-owned Property, Guest, Company, RateRestriction, CancellationPolicy, Folio, Payment. Each is a read-model maintained by sync consumers. Same pattern in channel-service (foreign RoomType, RatePlan, Reservation), guest-service (foreign Property), frontdesk-service (foreign Reservation).
  • paymentStatus is String? not enum on Reservation (hmp-reservation-service/prisma/schema.prisma:99) — typo silently produces garbage (Flow 3).
  • EInvoiceJob.ackDate is String? but Folio.ackDate is DateTime? — drift inside frontdesk-service.

4.8 @Public() exposure​

  • 102 @Public() decorators across services. Sample inspection shows most are legitimate (auth flows, JWKS, OIDC/SAML, billing webhook receiver, branding, partner session, JWT). Notable concerns:
    • hmp-asset-service/src/asset-registry/asset-registry-health.controller.ts:7 — @Public() on a health check is fine.
    • hmp-auth-service/src/billing/billing-webhook.controller.ts:16 — Razorpay billing webhook, must verify signature.
    • hmp-auth-service/src/branding/branding.controller.ts:31 — public branding.
    • hmp-auth-service/src/partner/organizations/partner-organizations.controller.ts:28 — VERIFY — public partner-organization CRUD raises eyebrows.
    • Reservation-service booking-engine endpoints: search/availability/calendar are REQUIRE_API_KEY=false (Flow 3 P0) — fully open.

4.9 Hard-coded secrets / repo hygiene​

  • JWT_PUBLIC_KEY hard-coded as PEM in docker-compose.dev-all.yml:32. Permanent in git history.
  • INTERNAL_SERVICE_TOKEN is HS256 in dev (per CLAUDE.md note), tracked as Phase 2 follow-up to RS256 before production.
  • No hard-coded Razorpay key/secret found in code (PASS).
  • "Default API key fallback 'hmp-default-public-booking-engine-key'" hard-coded in hmp-booking-engine/src/lib/api/client.ts:23 — DEV warning string, but permanent in master (Flow 3).

4.10 Boot-time guards (or absence thereof)​

GuardStatus
Hardware adapter requires real provider OR HARDWARE_MOCK=trueOK (hardware.module.ts:127-155)
EmailService requires SendGrid creds (AP-CC-5)OK
EventsGateway RS256 in production (JWT_PUBLIC_KEY)OK (gateway CLAUDE.md)
EINVOICE_MOCK !== 'true' in productionMISSING (Flow 8 P0)
SUBSCRIPTION_VIA_PAYMENT_SERVICE_ENABLED validatedN/A — flag does not exist in code (Flow 12)
ENCRYPTION_KEY required in frontdesk-serviceMISSING — falls back to plaintext (Flow 6 P1)

§5 — Top 20 P0 launch blockers, prioritised​

Effort estimates are gross — assume one experienced full-stack engineer-day per estimate.

#BlockerFilesOwnerEffortSuggested fix
1Booking last-room race — no inventory lock, two guests can both bookhmp-reservation-service/src/booking-engine/booking-engine.service.ts:981-1068, prisma/schema.prisma:187reservation3dSELECT FOR UPDATE per (propertyId, roomTypeId, date) inside createBooking transaction; add @@unique([propertyId, roomTypeId, date]) on a per-night inventory row; concurrency regression test
2Razorpay verifyPayment auto-confirms with no HMAC when key absenthmp-reservation-service/src/booking-engine/booking-engine.service.ts:1888-1938, 1741-1754reservation1dBoot-validate RAZORPAY_KEY_ID/SECRET for any property with booking-engine; remove mock-fallback branch; force HMAC fail-closed
3Tape-chart context menu + DetailPanel actions are stub toastshmp-web-dashboard/src/components/modules/front-desk/tape-chart-v2/tape-chart-v2-shell.tsx:339-372dashboard2dWire existing BarContextMenu + CellContextMenu from v1 tree; replace 'coming in Phase 2' toasts with real handler dispatch
4ID-photo capture broken end-to-end — data: URL rejected by @IsUrlhmp-frontdesk-service/src/checkin/dto/checkin.dto.ts:152-159, hmp-web-dashboard/.../check-in/[id]/keynova.tsx:444-446, 966frontdesk + dashboard2dImplement client-side S3 presigned upload before submit; send the public URL; or change DTO to @IsString() and re-upload server-side
5Empty-nationality foreign guest bypasses FRROhmp-frontdesk-service/src/checkin/checkin.service.ts:189-194frontdesk0.5dReject check-in with nationality === '' OR null regardless of path
6Walk-in path has no FRRO enforcementhmp-frontdesk-service/src/checkin/checkin.service.ts walkIn(); dto/checkin.dto.ts:212-278frontdesk1dAdd identical FRRO block to walkIn(); WalkInDto accepts formC
7Express check-in is a stubhmp-frontdesk-service/src/checkin/checkin.service.ts:691-752frontdesk1dEither implement full check-in or disable route until done; explicitly forbid for non-Indian nationality
8Police-register PII feed unimplementedhmp-frontdesk-service/src/reporting-feeds/reporting-feeds.service.ts:458frontdesk2dDecrypt formCDataEncrypted; populate passport/Aadhaar columns; respect DPDPA logging
9POS v2 GST split silently dropped at receiverhmp-frontdesk-service/src/folios/consumers/pos-folio-charge.consumer.ts:108-155, 373-383; hmp-frontdesk-service/src/folios/folios.service.ts:507-516frontdesk1dRead cgstAmount/sgstAmount/igstAmount/customerState from v2 payload; bypass getGSTRate re-derivation
10MenuItem missing HSN/SAChmp-pos-service/prisma/schema.prisma:111-139pos1d (schema) + 2d (UI/seed/migration)Add hsnCode String (and gstRate Decimal if per-item); migrate; surface in admin; propagate into v2 event
11IRN ack number overwrites sequential invoice numberhmp-frontdesk-service/src/einvoice/einvoice-job.processor.ts:64frontdesk0.5dDrop invoiceNumber: result.ackNumber write; keep sequential value, store ack in ackNumber only
12Two divergent invoice-number generatorshmp-frontdesk-service/src/folios/folios.service.ts:1620; invoice-pdf.service.ts:601-618frontdesk1dDelete the PDF-side generator; have it pull from FoliosService.getNextInvoiceNumber (or refactor into shared @hmp/common)
13No production guard against EINVOICE_MOCK=truehmp-frontdesk-service/src/einvoice/einvoice.module.tsfrontdesk0.25dBoot validator: throw when NODE_ENV='production' && EINVOICE_MOCK !== 'false'
14No split-payment support at checkouthmp-frontdesk-service/src/checkout/dto/checkout.dto.ts:30; checkout.service.ts:215-267frontdesk + dashboard3dExtend DTO to payments: { mode, amount, reference, cardLast4? }[]; loop inline settle; populate cardLast4/cardType; dashboard split-payment UI
15No invoice attachment on thank-you emailhmp-notification-service/src/dispatcher/dispatcher.service.ts:130-132notification + frontdesk1.5dPublish typed folio.settled event from checkout; dispatcher fetches PDF and attaches via SendGrid
16No FOLIO_PAYMENT_RECEIVED publish from inline checkouthmp-frontdesk-service/src/checkout/checkout.service.ts:236frontdesk0.5dPublish per-payment event; reservation-service FolioPaymentConsumer already wired
17Night audit no @@unique([propertyId, auditDate])hmp-frontdesk-service/prisma/schema.prisma:346-367frontdesk0.25d (schema) + 0.5d migrationMirror analytics-service constraint; remove "proceeding without lock" code path
18Night audit dedup window timezone math wronghmp-frontdesk-service/src/night-audit/night-audit.service.ts:453-455, 225frontdesk0.5dUse property-local midnight to property-local-tomorrow window
19Night audit reads averaged rate, not today's calendar ratehmp-frontdesk-service/src/night-audit/night-audit.service.ts:392-410frontdesk1dRead from RatePlanRate for the night via PropertyServiceClient
20KPI tile reportIds wrong — every Operations Hub tile 404shmp-web-dashboard/src/components/modules/operations-hub/tiles/kpi/{revpar,adr,occupancy,arrivals-today,departures-today,inhouse-count,no-shows-today}.tile.tsxdashboard0.5dFix to manager-flash-report, arrival-report, departure-report, in-house-report, no-show-reservation-report; add Vitest contract test

Sub-total of top-20 P0 effort: ≈ 24 engineer-days.


§6 — Effort summary​

The top-20 P0 list above is not the complete P0 set — there are ~73 P0 findings across the 13 flows. Rough aggregate:

BucketCountAggregate effort
Top-20 P0 (above)20~24 engineer-days
Remaining P0 (53 across 13 flows + cross-cutting)~53~70-90 engineer-days
All P0 combined~73~95-115 engineer-days
P1 findings (estimate)~80~50-70 engineer-days
All P0 + P1~153~145-185 engineer-days

For a team of 5 engineers running parallel, focused on launch readiness:

  • All P0 done in 4-6 weeks (~95-115 engineer-days / 5 engineers).
  • All P0 + P1 done in 6-8 weeks (~145-185 engineer-days / 5 engineers).

This is a launch-readiness arc, not a sprint. The gold-standard charter (per hmp-docs/docs/platform-registry/property-1-gold-standard-fix-plan-2026-04-28.md) further requires that every fix include CLAUDE.md updates, platform-registry updates, tests-before-fix, idempotency at every boundary, and "one canonical impl not two". Assume +30-50% effort if those requirements are followed, which they should be.

Honest verdict: property #1 cannot launch on this codebase in the next 4 weeks without explicit acceptance of multiple regulatory and business-correctness gaps. A 6-week focused arc is the minimum viable runway, and only if the work is gated by the gold-standard charter.


§7 — Appendix: verification log​

Phase A inventory commands:

  • git -C <repo> rev-parse --short HEAD for all 24 repos.
  • find . -maxdepth 4 -name "schema.prisma" -path "*/prisma/*" (17 schemas).
  • wc -l hmp-api-gateway/src/config/services.config.ts hmp-api-gateway/src/proxy/proxy.controller.ts → 1468 + 2551 lines.
  • grep -n "model |enum " <each schema>.
  • grep "SKIP_EVENT_SUBSCRIPTIONS|HARDWARE_MOCK|EINVOICE_MOCK|JWT_PUBLIC_KEY|INTERNAL_SERVICE_TOKEN" docker-compose.*.yml.

Phase B per-flow files opened (not exhaustive — see each flow's section for full citations):

  • Flow 1 (Tenant): hmp-auth-service/src/tenants/, src/onboarding/, src/billing/, src/subscriptions/, prisma/schema.prisma, hmp-shared-packages/.../service-clients/property-service.client.ts:530-550, hmp-admin-portal/src/app/(auth)/signup/page.tsx, hmp-admin-portal/src/app/(admin)/tenants/[id]/page.tsx.
  • Flow 2 (Property): hmp-property-service/prisma/schema.prisma, src/properties/, src/rooms/, src/rate-plans/, src/cancellation-policies/, src/tax/, hmp-web-dashboard/src/app/(dashboard)/settings/property/page.tsx, hmp-booking-engine/src/pages/direction-b/RoomDetailPage.tsx.
  • Flow 3 (Booking engine): hmp-reservation-service/src/booking-engine/, prisma/schema.prisma, hmp-payment-service/src/, hmp-booking-engine/src/lib/api/client.ts, dashboard reservations API.
  • Flow 4 (Verify reservation): hmp-reservation-service/src/reservations/, dashboard (dashboard)/reservations/keynova.tsx, lib/api/reservations.ts, lib/hooks/use-global-search.ts.
  • Flow 5 (HK / room move / extend): hmp-frontdesk-service/src/room-ops/, src/upgrades/, src/checkin/checkin.controller.ts, hmp-housekeeping-service/src/gateway/housekeeping.gateway.ts, hmp-mobile-apps/apps/housekeeping/, dashboard tape-chart drag actions.
  • Flow 6 (Check-in): hmp-frontdesk-service/src/checkin/, src/cform/, src/sponsors/, src/reporting-feeds/, hmp-guest-service/src/guests/, hmp-shared-packages/packages/integrations/.../frro/manual-upload-frro-client.ts, dashboard check-in page.
  • Flow 7 (Room service): hmp-pos-service/src/orders/, src/menus/, src/gateway/kitchen.gateway.ts, hmp-frontdesk-service/src/folios/consumers/pos-folio-charge.consumer.ts, src/folios/folios.service.ts, dashboard folio page.
  • Flow 8 (Check-out): hmp-frontdesk-service/src/checkout/, src/folios/, src/einvoice/, src/late-checkout/, hmp-shared-packages/.../printing/receipt-templates.ts, dispatcher.service consumer.
  • Flow 9 (Night audit): hmp-frontdesk-service/src/night-audit/, src/night-audit-pdf.service.ts, night-audit-scheduler.service.ts, hmp-analytics-service/src/consumers/night-audit.consumer.ts, dashboard night-audit page.
  • Flow 10 (Reports): hmp-analytics-service/src/consumers/, src/reporting/reports/shared/period-helpers.ts, src/reporting/exports/, dashboard (dashboard)/reports/v2/, dashboard components/modules/operations-hub/tiles/kpi/*.tile.tsx.
  • Flow 11 (ResAvenue): hmp-channel-service/src/intermediary/, src/sync/, src/intermediary/providers/resavenue/, prisma/schema.prisma, hmp-docs/Integrations/ResAvenue/cert-evidence-2026-05-05.md, admin portal channels pages.
  • Flow 12 (Payments): hmp-payment-service/src/, prisma/schema.prisma, src/adapters/pinelabs/, src/adapters/razorpay/, src/webhooks/, src/orchestrator/, hmp-shared-packages/.../sanitizer/pci-sanitizer.ts, dashboard folio TerminalChargeModal, booking-engine PaymentForm.
  • Flow 13 (Tape Chart): hmp-web-dashboard/src/components/modules/front-desk/tape-chart-v2/, tape-chart/ (legacy), hmp-frontdesk-service/src/calendar/, hmp-reservation-service/prisma/schema.prisma.

Phase C cross-cutting commands:

  • grep -rE "prisma\.(folio|payment|property|guest|roomType|ratePlan)\." hmp-channel-service/src hmp-reservation-service/src → 79 matches.
  • grep -rn "@Public()" hmp-*-service/src --include="*.controller.ts" → 102 matches.
  • grep -E "tenantId\s+String\s+@db\.Uuid" <all schemas> → consistent (PASS).
  • grep "build" hmp-{property,reservation,frontdesk}-service/package.json → tsc -p tsconfig.docker.json || true (P0).
  • grep -rn "import type" hmp-*-service/src --include="*.ts" | grep "Service|Module|Controller" → no DI metadata erasure (PASS).

End of report. Audit conducted under explicit instruction to distrust all prior documentation and verify against actual code, schema, and UI. Verdict is fact-based and unflattering by design. Owner action: triage the top-20 P0 list against headcount × calendar runway and decide whether to proceed, descope, or delay launch.