Skip to main content

Read-Model Include Sweep — #7598 (2026-06-07)


SWEEP CLOSURE (2026-06-08)​

All items resolved. Deployed to test and probed 2026-06-08.

TierCountFinal state
P013All 13 Closed-Verified (individual Boards items #7598 links)
P137All 37 FIXED + deployed (9 clusters, commits listed per item)
P21713 FIXED · 2 DESIGNED (P2-1 company publisher + P2-14 engagement events) · 2 documented data-path gaps (FeedbackSurvey hydration follow-up; CRM RoomReservation sync deferred)
info21 dead-code confirmed (no action); 1 unnecessary cast (style only)

Hardening chain (surfaced and closed during batch-8):

  • #7607 (P0) — corporate room-charge fail-closed (folioType = 'GUEST' filter blocked all COMPANY folios) → FIXED frontdesk b4e4950 → Closed-Verified
  • #7608 (P1) — ArgoCD hmp-_shared Application not generated on test (invalid Kubernetes name hmp-_shared) → filed, hand-patched live, permanent fix pending (rename _shared → shared)
  • #7609 — B2 Secrets Manager secret rotation gap → DESIGNED (infra spec)
  • #7610 (security) — company CRUD mass-assignment: CreateCompanyDto/UpdateCompanyDto whitelist missing → FIXED + Closed-Verified

Non-Findings (NF) disposition:

  • NF-1: minibar NOT gateway-routed (two-file-rule gap) — P3, dormant, documented
  • NF-2: /admin/properties/:id/transfer NOT gateway-routed — P3, dormant, documented
  • NF-3: recommendations/generate unvalidated @Body — FIXED (same-day, P2)
  • NF-4: e-invoice-report IST/UTC window mismatch — P3, documented
  • NF-5: #7607 corporate room-charge (P0, fixed same-day)
  • NF-6: ArgoCD _shared Application failure — P1, hand-patched, #7608 filed
  • NF-7: GroupRoomingDto missing confirmationNumber/arrivalDate/departureDate — P3, open

GOLD count at sweep close: 37 (US-005B-07 ✅ GOLD #37 earned during batch-8).

FU-86 (property branding sync — brandName/logoUrl/primaryColor not carried in TENANT_* events → null in effective-branding response) remains open.


15-service automated sweep for the cross-service-Prisma-include / phantom-read-model defect class, triggered by THREE live production-path crashes in one day (#7592 groups company-include, #6489-follow-up Company.creditLimit select, #7597 no-show ratePlan include — the third killed every nightly audit since ship). Method: per-service agent validated every any-casted prisma call's include/select/field args against the LOCAL schema.prisma + checked read-model relations for writers.

TALLY: 13 P0 · 37 P1 · 17 P2 · 2 info. P0s are filed as individual Boards items (see #7598's links); P1/P2 are worked from THIS document under the #7598 umbrella, batch by batch.

Rule (now in multiple service CLAUDE.mds): the slim per-service schema is the ONLY truth — an any-cast that dodges tsc crashes at runtime ("Unknown field/argument", undefined accessor) or silently null-joins on phantom never-populated read models. Cross-service data = ServiceClient.

P0 (13)​

hmp-frontdesk-service — night-audit/chain-audit-orchestrator.service.ts : 260-263​

  • TenantReadModel.tenant (accessor name wrong + phantom fields) — missing-relation-crash
  • Evidence: Code calls this.xPrisma.tenant.findUnique({ where: { id: tenantId }, select: { id: true, settings: true } }). Schema has model TenantReadModel { tenantId String @id ... @@map("tenant_read_model") } — Prisma generates accessor prisma.tenantReadModel, NOT prisma.tenant. prisma.tenant is undefined at runtime → undefined.findUnique(...) throws immediately. Additionally, even if the accessor were corrected: (a) the PK is tenantId not id, so where: { id: tenantId } would fail; (b) settings is not a column on TenantReadModel (it has only tenantId/name/code/isActive/isDemoTenant/status/syncedAt). The STRICT-TODO comment (line 60-62) acknowledges the intent was to read the auth-service full Tenant entity (which does carry settings), but no such model exists in this service's schema.
  • Reachability: POST /night-audit/chain (controller) → ChainAuditOrchestratorService.runChainAudit() → xPrisma.tenant.findUnique(). Also reached by the K8s CronJob (night-audit-cronjob.yaml) that POSTs this endpoint on a nightly schedule. Every chain audit invocation crashes here.

hmp-frontdesk-service — company-ledger/company-ledger.service.ts : 114-119​

  • Company.currentBalance (phantom column) — missing-relation-crash
  • Evidence: Inside postCompanyPayment → this.prisma.$transaction, tx.company.update({ where: { id: dto.companyId }, data: { currentBalance: { increment: dto.amount } } }). The local Company read-model schema (prisma/schema.prisma lines 918-932) has exactly: id, propertyId, name, contactPerson, email, gstin, isActive, createdAt, updatedAt, folios[]. NO currentBalance column. Prisma rejects an update with an unknown field at query-validation time (the error fires even if zero Company rows exist) → the entire $transaction rolls back → folio payment NOT posted, Payment record NOT created, folio balance NOT decremented.
  • Reachability: POST /payments/company (company-ledger.controller.ts @Post('payments/company')) → CompanyLedgerService.postCompanyPayment(). Any CITY_LEDGER corporate payment attempt will crash here.

hmp-frontdesk-service — company-ledger/company-ledger.service.ts : 226-231​

  • Company.currentBalance (phantom column) — missing-relation-crash
  • Evidence: xPrisma.company.update({ where: { id: companyId }, data: { currentBalance: { decrement: dto.amount } } }) in settleCompanyBalance. Same root cause as line 114-119: Company.currentBalance does not exist in the schema → Prisma unknown-field error at query validation time → 500 on every call.
  • Reachability: POST /companies/:id/settle (company-ledger.controller.ts @Post('companies/:id/settle')) → CompanyLedgerService.settleCompanyBalance(). Crashes on every settlement attempt.

hmp-reservation-service — payments/payments.service.ts : 197​

  • FolioTransaction.folioTransaction (model not in local schema) — missing-relation-crash
  • Evidence: CrossServicePrisma = any; line 197 calls tx.folioTransaction.create(...) inside an xPrisma.$transaction. folioTransaction is not a model in prisma/schema.prisma (belongs to hmp-frontdesk-service). At runtime prisma.folioTransaction is undefined → TypeError inside the transaction. Companion issue: the payment.create at line 169 also supplies receivedBy: 'SYSTEM' which is not a field on the local Payment model (schema lines 586-607 — only id, reservationId, paymentMode, amount, currency, reference, referenceHash, status, receivedAt, createdAt), causing an earlier P2019 before folioTransaction is reached.
  • Reachability: POST /payments/verify → PaymentsService.verify(). This is the online payment verification endpoint called after Razorpay confirms payment. The transaction fails and the payment is never recorded despite Razorpay charging the guest. Money/compliance path.

hmp-reservation-service — payments/payments.service.ts : 396​

  • FolioTransaction.folioTransaction (model not in local schema) — missing-relation-crash
  • Evidence: Same as line 197 — tx.folioTransaction.create(...) inside webhook handler's xPrisma.$transaction. folioTransaction not in local schema. Also receivedBy: 'WEBHOOK' (line 358) is not a field on the local Payment model — Prisma throws P2019 on the payment.create step first, before reaching folioTransaction.
  • Reachability: POST /payments/webhook → PaymentsService.handleWebhook() → payment.captured branch. This is the Razorpay server-to-server callback that records payment after online booking. Every webhook confirmation of payment crashes; the reservation stays PENDING and the guest's money is not recorded. Public endpoint (no JWT). Compliance/money path.

hmp-housekeeping-service — minibar/minibar.service.ts : 53, 69, 91, 115, 131, 161, 180​

  • STATUS: FIXED #7600 (2026-06-07). CLOSED-VERIFIED 2026-06-08 — in-cluster probes: GET 200 [] (empty minibar, crash-free), consume unknown-item clean 404, replenish 200, charge 200 honest no-consumption. NOTE: minibar module is NOT gateway-routed (no /minibar prefix in services.config.ts + ProxyController — two-file-rule gap, dormant surface). Recorded as NEW FINDING (2026-06-08), see below. Root-cause fix = denormalize (option a): added MinibarItem.unitPrice scalar (migration 20260607150000_add_minibar_item_unit_price, idempotent), itemName already existed; dropped the phantom item @relation + every include: { item: true }; removed @ts-nocheck; code now reads row scalars. Charge path hardened: resolveUnitPrice throws 422 UnprocessableEntityException when price is missing/≤0 — never posts a ₹0/null line. Tests use non-permissive mocks (item undefined). tsc clean, suite 219 green. See housekeeping CLAUDE.md gotcha #10.
  • MinibarItem.item (→ InventoryItem) — phantom-readmodel-relation
  • Evidence: MinibarItem.item IS declared in schema.prisma (line 193: item InventoryItem @relation(...)) so include: { item: true } does not throw 'Unknown field'. However InventoryItem is documented as a 'cross-service read model stub from hmp-pos-service' (tenant-export.service.ts:18) and NO consumer, sync service, or current seed populates the housekeeping DB's inventory_items table at runtime. The only seed that ever inserted inventory_items was seeds/archive/seed-enhanced-data.sql (archived, not run by seed-master.sh). The result: every include: { item: true } silently returns item=null. minibar.service.ts then unconditionally dereferences minibarItem.item.name (line 98) and minibarItem.item.unitCost (lines 101-102) — TypeError: Cannot read properties of null at runtime on any minibar read/consume/charge path. The file carries @ts-nocheck (line 1) to suppress TS warnings, explicitly acknowledging the defect. inventory.service.ts lines 170-171 also dereference minibarItem.item.unitCost and minibarItem.item.name after the same include.
  • Reachability: GET /api/minibar/:roomId → MinibarService.getByRoom (line 51); POST /api/minibar/:roomId/consume → MinibarService.updateConsumption (line 67); POST /api/minibar/:roomId/replenish → MinibarService.replenish (line 113) and replenishAll (line 161); POST /api/minibar/:roomId/charge → MinibarService.chargeToFolio (line 179). Legacy paths: GET/POST /api/inventory/minibar/:roomId → InventoryService.getMinibar (line 141) and updateMinibar (line 149). All six minibar endpoints crash on the first hotel with any minibar items configured.

hmp-housekeeping-service — inventory/inventory.service.ts : 141, 149, 165​

  • STATUS: FIXED #7600 (2026-06-07). CLOSED-VERIFIED 2026-06-08 (same probe set as minibar.service.ts above). getMinibar/updateMinibar drop the item include and read itemName/unitPrice; updateMinibar throws 422 on missing/≤0 price before publishing the folio charge.
  • MinibarItem.item (→ InventoryItem) — phantom-readmodel-relation
  • Evidence: Same phantom-readmodel defect as minibar.service.ts. InventoryService.getMinibar (line 141) and updateMinibar (line 149) both call include: { item: true } on MinibarItem. updateMinibar then dereferences minibarItem.item.unitCost (line 170) and minibarItem.item.name (line 172) unconditionally — TypeError when item=null. inventory_items table is never written to by any runtime path in this service.
  • Reachability: GET /api/inventory/minibar/:roomId → InventoryService.getMinibar; POST /api/inventory/minibar/:roomId → InventoryService.updateMinibar (legacy minibar path, both active per CLAUDE.md gotcha #2).

hmp-guest-service — gdpr/gdpr.service.ts : 117-124​

  • STATUS: FIXED #7601 (2026-06-07). CLOSED-VERIFIED 2026-06-08 — DELETE /gdpr/guests/:id/erasure 200; PII fully anonymized (GDPR_ERASED + hashed email).
  • Company.contactPerson — missing-relation-crash
  • Evidence: gdpr.service.ts line 120 writes contactPerson: 'REDACTED' in a tx.company.updateMany({ data: { contactPerson, email, phone } }) inside a $transaction(async (tx: any) => ...). The guest-service Company model (schema.prisma lines 103-125) has columns name, code, email, phone, gstin, address, city, state, country, postalCode, creditLimit, isActive — NO contactPerson column exists. tx is typed any so tsc is silent. Prisma throws "Unknown argument contactPerson" at runtime, aborting the entire $transaction. The guest's PII anonymisation, the guestIdentity erasure, the consentLog write, and the gdprErasureLog row are ALL rolled back. The endpoint returns a 500.
  • Reachability: DELETE /gdpr/guests/:id/erasure (GdprController.eraseGuestData, Roles SUPER_ADMIN/ADMIN) AND PATCH /dpdp/requests/:id/status (DpdpService.processRequest → GdprService.eraseGuestData when requestType=ERASURE). Triggers whenever the targeted guest has a non-null companyId.

hmp-property-service — rates/bar-engine.service.ts : 378-409​

  • STATUS: FIXED #7603 (2026-06-07). CLOSED-VERIFIED 2026-06-08 — POST /rates/recommendations/generate → 201 total:4; rows persist source=BAR + demandFactor/seasonMultiplier (the write that crashed the nightly cron). NEW FINDING (2026-06-08): the generate handler binds @Body to a raw TS type — ZERO validation. See NEW FINDINGS section below.
  • PricingRecommendation.fields: forecastOccupancy, demandFactor, seasonMultiplier (all absent from schema) + source enum value 'BAR' (schema only has FORECAST/HEURISTIC/RM_OVERRIDE) — missing-relation-crash
  • Evidence: schema.prisma PricingRecommendation (line 1132-1155) has NO forecastOccupancy, demandFactor, or seasonMultiplier columns. The PricingRecommendationSource enum (line 1126-1130) lists only FORECAST/HEURISTIC/RM_OVERRIDE — 'BAR' is not a member. bar-engine.service.ts casts to (this.prisma as any).pricingRecommendation and upserts with all three missing fields plus the invalid enum value. Prisma will throw 'Unknown field X' at runtime on first upsert attempt. The where key propertyId_roomTypeId_date_source is structurally correct (@@unique([propertyId, roomTypeId, date, source])), but the body fields are wrong.
  • Reachability: RecommendationGenerationCron @Cron('0 6 * * *', {timeZone:'Asia/Kolkata'}) runs daily for every active property x room type. Also called from rate-calendar.service.ts generateRecommendations path reached via GET /rates/calendar. Crashes silently per-room-type inside a try/catch but logs a WARN each time — the cron runs but produces zero stored recommendations.

hmp-property-service — admin/property-transfer.service.ts : 89-113​

  • STATUS: FIXED #7604 (2026-06-07). CLOSED-VERIFIED 2026-06-08 — non-permissive unit suite (1021 green) proves no phantom User/AuditLog writes; in-cluster probes: empty body 400, nonexistent property handler-404. NEW FINDING (2026-06-08): /admin/properties/:id/transfer NOT gateway-routed (only /admin/providers is in services.config.ts + ProxyController) — dormant operator surface. See NEW FINDINGS section below.
  • User / AuditLog.tx.user (model 'user' absent from schema) + tx.auditLog (model 'auditLog' absent from schema) — missing-relation-crash
  • Evidence: The property-service schema.prisma defines no 'User' or 'AuditLog' model. property-transfer.service.ts line 81 opens a $transaction and at line 89 calls tx.user.updateMany({...}) then at line 95 calls tx.auditLog.create({...}). Both will throw Prisma P2009 'Cannot find model' on first execution. The transaction is entered as CrossServicePrisma (type alias for any) so TypeScript cannot catch this.
  • Reachability: POST /admin/properties/:id/transfer controller route (property-transfer.controller.ts line 25, @Roles('SUPER_ADMIN')). Any SUPER_ADMIN attempting to transfer a property hits the crash inside the transaction — the transfer never completes.

hmp-notification-service — consumers/guest-erased.consumer.ts : 72-90​

  • STATUS: FIXED #7602 (2026-06-07). CLOSED-VERIFIED 2026-06-08 — payment consumer processed instantly ('Erased PII ... PaymentLink + Payment.metadata'); notification consumer processed with ~59s stream-poll lag ('... NotificationLog + Guest + WhatsAppConversation'); Redis XINFO both groups lag 0 pending 0.
  • Guest.tenantId (where clause field) — missing-relation-crash
  • Evidence: GuestErasedConsumer.handleGuestErased() wraps three updateMany calls in a single $transaction([...]). The second call is (this.prisma as any).guest.updateMany({ where: { id: guestId, tenantId } }). The local Guest model (prisma/schema.prisma lines 136-149) has fields: id, propertyId, firstName, lastName, email, phone, secondaryPhone — NO tenantId column. Prisma throws PrismaClientValidationError: 'Unknown argument tenantId in where' at transaction-build time, which aborts the entire $transaction. The third call — (this.prisma as any).whatsAppConversation.updateMany({ where: { guestId, tenantId } }) — references tenantId on WhatsAppConversation (schema lines 289-304: propertyId, guestPhone, guestId, guestName, status, messages — also NO tenantId). Both failures cause the batch transaction to never execute any of the three operations, so Guest PII (firstName/lastName/email/phone), WhatsApp conversation identifiers (guestName/guestPhone), AND NotificationLog.recipient are never erased. The Kafka handler at line 43-55 catches the error and logs it, so the service does not crash, but every guest.erased event silently fails to erase PII in all three tables. This is an incomplete GDPR-erasure path on every invocation.
  • Reachability: Kafka consumer on topic 'guest.erased'. Triggered by hmp-guest-service GDPR right-to-erasure flow (DSAR deletion). Every GDPR deletion request since ship silently leaves Guest PII and WhatsApp conversation data un-erased in this service.

hmp-payment-service — consumers/guest-erased.consumer.ts : 64-78​

  • STATUS: FIXED #7602 (2026-06-07). CLOSED-VERIFIED 2026-06-08 (same probe — payment consumer processed instantly, XINFO lag 0).
  • PaymentLink.guestId — missing-relation-crash
  • Evidence: GuestErasedConsumer.handleGuestErased() calls (this.prisma as any).paymentLink.updateMany({ where: { guestId, tenantId } }) and (this.prisma as any).payment.updateMany({ where: { guestId, tenantId } }) via (this.prisma as any) to bypass TypeScript. Neither PaymentLink nor Payment in prisma/schema.prisma has a guestId column — verified: Payment fields are id, tenantId, propertyId, useCase, externalReferenceType, externalReferenceId, amount, currency, status, successfulAttemptId, idempotencyKey, metadata, createdBy, createdAt, updatedAt, capturedAt, failedAt, refundedAt, merchantAccountId; PaymentLink fields are id, paymentId, tenantId, propertyId, slug, purpose, useCase, externalRefType, externalRefId, amountDue, currency, allowPartial, minimumAmount, allowTipping, recipientName, recipientEmail, recipientPhone, language, sendChannels, lastSentAt, sendCount, status, expiresAt, providerRef, providerUrl, batchId, amountPaid, tipAmount, paidAt, createdBy, cancelledBy, cancelledAt, cancelReason, createdAt, updatedAt. At runtime Prisma throws 'Invalid prisma.paymentLink.updateMany() invocation — Unknown field guestId'. The error propagates up to the subscriber try/catch (line 48), is logged as an error, and handleGuestErased() exits before reaching line 85 (kafka.publish guest.erased.ack). GDPR/DSAR erasure silently fails: PaymentLink.recipientName/Email/Phone is never nulled, Payment.metadata is never cleared, and guest.erased.ack is never published — breaking the compliance acknowledgement chain.
  • Reachability: Event consumer: guest.erased topic (AP-T08-005). Triggered by hmp-guest-service DSAR erasure flow on every right-to-erasure request. Registered in AppModule as a GuestErasedConsumer provider. Fires on every real guest erasure; the subscriber callback swallows the Prisma error so the payment-service stays alive but the erasure is silently a no-op on every call.

hmp-spa-service — activities/activities.service.ts : 232-247​

  • STATUS: FIXED #7605 (2026-06-07). CLOSED-VERIFIED 2026-06-08 — full lifecycle live: create activity → book 201 with real guestId → list → cancel.
  • ActivityBooking.guest (via guestId FK → guests table) — missing-relation-crash
  • Evidence: activityBookingModel.create({ data: { ..., guestId: dto.guestId, ... }, include: { activity: true, guest: true } }) is called via (this.prisma as any).activityBooking (line 28). The DB migration 20260526130000_add_activities_activity_bookings_guests/migration.sql defines activity_bookings.guestId as UUID NOT NULL with a hard FK constraint (activity_bookings_guestId_fkey → guests.id ON DELETE RESTRICT). The Guest model exists in schema.prisma (lines 264-278) as a read model, but there is NO consumer or sync service in hmp-spa-service that writes (create/upsert/createMany) rows into the guests table — only guest-erased.consumer.ts:75 runs updateMany (no-op on empty table). Therefore every call to book() passes a guestId that has no corresponding row in guests, and Prisma throws P2003 (foreign-key constraint violation) on the DB INSERT. Confirmed: seed-keynova-spa.sql inserts into spa_treatments/spa_therapists/spa_rooms/spa_appointments only — zero INSERT INTO guests.
  • Reachability: POST /api/spa/activities/:id/bookings → ActivitiesController.book() → ActivitiesService.book()

P1 (37)​

hmp-frontdesk-service — payments/payment-reconciliation.service.ts : 36-44​

  • STATUS: FIXED #7598 P1 (2026-06-08) — commit 9f7e015. postedBy + reference fields now included in create; suite green.
  • FolioTransaction.postedBy (required field missing from create) — missing-relation-crash
  • Evidence: (this.prisma as any).folioTransaction.create({ data: { folioId, transactionType, transactionCode, description, amount, postingDate } }) — postedBy String is declared at schema line 146 with no @default(), making it a required NOT NULL column. The create omits it entirely. Prisma will throw a required-field validation error at runtime on every call to this endpoint.
  • Reachability: POST /payments/record (PaymentReconciliationController @Post('record')) → PaymentReconciliationService.recordPayment(). PaymentReconciliationModule is registered in app.module.ts (line 109). Crashes on every invocation.

hmp-frontdesk-service — company-ledger/company-ledger.service.ts : 68-72​

  • Company.currentBalance + creditLimit (phantom columns, silent data integrity failure) — phantom-readmodel-relation
  • Evidence: Number(company.currentBalance) and Number(company.creditLimit) read from the bare xPrisma.company.findUnique result. Schema confirms these columns do not exist on Company. At runtime company.currentBalance and company.creditLimit are both undefined. Number(undefined) = NaN. The credit-limit guard Number(company.creditLimit) > 0 && newBalance > Number(company.creditLimit) evaluates to NaN > 0 = false — the guard silently never fires. Every corporate payment proceeds without any credit ceiling check, even when the company is over-limit. This allows unlimited corporate debt accrual with no guard. Note: this does NOT crash (it's a silent no-op), but the folio-city-ledger accounting is already broken at line 114 (the crash finding above).
  • Reachability: POST /payments/company → CompanyLedgerService.postCompanyPayment() — reached before the crashing update at line 114. Also GET /companies/:id/ledger returns currentBalance: NaN in the response (line 193-194).

hmp-reservation-service — waitlist/waitlist.service.ts : 566​

  • STATUS: FIXED #7598 P1 (2026-06-08) — commit f4ba58e. Replaced xPrisma.ratePlan.findFirst with ServiceClient read; suite green.
  • RatePlan.ratePlan (model deleted Phase 2 Sprint 1 — not in local schema) — missing-relation-crash
  • Evidence: CrossServicePrisma = any; line 566 calls (this.prisma as CrossServicePrisma).ratePlan.findFirst(...). ratePlan is not a model in prisma/schema.prisma (the NOTE at schema line 509 explicitly states RatePlan was deleted). At runtime this.prisma.ratePlan is undefined, so .findFirst throws TypeError: Cannot read properties of undefined (reading 'findFirst').
  • Reachability: POST /waitlist/:id/accept → WaitlistService.acceptOffer() → getDefaultRatePlanId() at line 398. Staff-facing (RESERVATIONS+) route registered in WaitlistModule which is imported in AppModule.

hmp-reservation-service — groups/groups.service.ts : 232-233​

  • STATUS: FIXED #7598 P1 (2026-06-08) — commit f4ba58e. Removed phantom include relations on GroupRoomAllocation; suite green.
  • GroupRoomAllocation.roomType, ratePlan (soft UUID refs — no @relation on GroupRoomAllocation) — missing-relation-crash
  • Evidence: GroupRoomAllocation in schema (lines 372-386) has only roomTypeId String @db.Uuid and ratePlanId String @db.Uuid — no roomType/@relation and no ratePlan/@relation. findOne() at line 227 uses xPrisma.group.findUnique with include: { roomAllocations: { include: { roomType: {...}, ratePlan: {...} } } }. Prisma throws 'Unknown field roomType for include statement on model GroupRoomAllocation' at runtime on every GET /groups/:id call.
  • Reachability: GET /groups/:id → GroupsService.findOne(). Also present at lines 599-601, 618-619 (GET /groups/:id/rooming → getGroupRooming), lines 779-780 (GET /groups/:id/cutoff-status → getCutoffStatus), lines 835-836, 843-844 (GET /groups/:id/analytics → getGroupAnalytics). All group detail/analytics endpoints crash on first request.

hmp-reservation-service — groups/groups.service.ts : 249-250​

  • STATUS: FIXED #7598 P1 (2026-06-08) — commit f4ba58e. Same fix as 232-233 above; suite green.
  • RoomReservation.roomType, room (soft UUID refs — no @relation on RoomReservation) — missing-relation-crash
  • Evidence: RoomReservation in schema (lines 210-237) has only roomTypeId String @db.Uuid and roomId String? @db.Uuid — no roomType/@relation and no room/@relation. Lines 247-252 in findOne() include roomReservations: { include: { roomType: {...}, room: {...} } }. Prisma throws 'Unknown field roomType for include statement on model RoomReservation' at runtime. Same pattern at lines 617-619 (getGroupRooming) and line 1123 (allocateRooms).
  • Reachability: GET /groups/:id → findOne(). GET /groups/:id/rooming → getGroupRooming(). POST /groups/:id/allocate-rooms → allocateRooms(). All staff-facing group endpoints crash.

hmp-reservation-service — groups/groups.service.ts : 1378​

  • STATUS: FIXED #7598 P1 (2026-06-08) — commit f4ba58e. Removed phantom Folio.transactions include; suite green.
  • Folio.transactions (no @relation on local Folio model) — missing-relation-crash
  • Evidence: Local Folio model (schema lines 570-584) has only: id, reservationId, reservation, folioNumber, guestName, totalCharges, totalPayments, balance, status, createdAt, updatedAt — no transactions relation. getGroupFolio() at line 1376 includes folios: { include: { transactions: { where: { isVoid: false } } } }. Prisma throws 'Unknown field transactions for include statement on model Folio' at runtime.
  • Reachability: GET /groups/:id/folio → GroupsService.getGroupFolio(). Staff-facing group master folio endpoint crashes on every request.

hmp-reservation-service — guest-portal/guest-portal.controller.ts : 47-49​

  • STATUS: FIXED #7598 P1 (2026-06-08) — commits 1332388 + 919e320. GuestPortalModule migrated to ServiceClient reads; honest semantics; suite green.
  • RoomReservation.roomType, room, ratePlan (all soft UUID refs — no @relation on RoomReservation) — missing-relation-crash
  • Evidence: lookupReservation() at line 35 includes roomReservations: { include: { roomType: {...}, room: {...}, ratePlan: {...} } }. None of these are relations on the local RoomReservation model (schema lines 210-237). Prisma throws 'Unknown field roomType for include statement' at runtime on first guest portal lookup.
  • Reachability: POST /guest-portal/lookup → GuestPortalController.lookupReservation(). Public endpoint (@Public + @RequireApiKey). GuestPortalModule is imported in AppModule. Every guest self-service portal lookup crashes.

hmp-reservation-service — guest-portal/guest-portal.controller.ts : 115​

  • STATUS: FIXED #7598 P1 (2026-06-08) — commits 1332388 + 919e320 (same as above).
  • RoomReservation.room (soft UUID ref — no @relation on RoomReservation) — missing-relation-crash
  • Evidence: submitRequest() at line 113 includes roomReservations: { include: { room: true } }. room is not a relation on the local RoomReservation model (only roomId String? @db.Uuid). Prisma throws 'Unknown field room for include statement' at runtime.
  • Reachability: POST /guest-portal/requests → GuestPortalController.submitRequest(). Public guest-facing endpoint. Crashes on every guest maintenance/amenity request submission.

hmp-reservation-service — guest-portal/guest-portal.controller.ts : 125​

  • STATUS: FIXED #7598 P1 (2026-06-08) — commits 1332388 + 919e320. xPrisma.maintenanceRequest replaced with ServiceClient (hmp-asset-service); suite green.
  • MaintenanceRequest.maintenanceRequest (model not in local schema) — missing-relation-crash
  • Evidence: CrossServicePrisma = any; line 125 calls this.xPrisma.maintenanceRequest.create(...). maintenanceRequest is not a model in prisma/schema.prisma (belongs to hmp-asset-service). At runtime this.prisma.maintenanceRequest is undefined, so .create throws TypeError: Cannot read properties of undefined.
  • Reachability: POST /guest-portal/requests → GuestPortalController.submitRequest(). Even if the room include at line 115 is somehow bypassed, this crashes immediately after. Public guest-facing endpoint.

hmp-reservation-service — guest-portal/guest-portal.controller.ts : 226​

  • STATUS: FIXED #7598 P1 (2026-06-08) — commits 1332388 + 919e320. xPrisma.guestIdentity replaced with ServiceClient (hmp-guest-service); suite green.
  • GuestIdentity.guestIdentity (model not in local schema) — missing-relation-crash
  • Evidence: CrossServicePrisma = any; line 226 calls this.xPrisma.guestIdentity.upsert(...). guestIdentity is not a model in prisma/schema.prisma (belongs to hmp-guest-service). At runtime this.prisma.guestIdentity is undefined → TypeError.
  • Reachability: POST /guest-portal/pre-checkin → GuestPortalController.preCheckIn(). Public guest-facing endpoint. Crashes whenever a guest submits an ID document during pre-check-in.

hmp-reservation-service — guest-portal/guest-portal.controller.ts : 246​

  • STATUS: FIXED #7598 P1 (2026-06-08) — commits 1332388 + 919e320. xPrisma.guestPreference replaced with ServiceClient (upsertGuestPreference from @hmp/service-clients@1.28.3); suite green.
  • GuestPreference.guestPreference (model not in local schema) — missing-relation-crash
  • Evidence: CrossServicePrisma = any; line 246 calls this.xPrisma.guestPreference.upsert(...). guestPreference is not a model in prisma/schema.prisma (belongs to hmp-guest-service). At runtime this.prisma.guestPreference is undefined → TypeError.
  • Reachability: POST /guest-portal/pre-checkin → GuestPortalController.preCheckIn(). Public guest-facing endpoint. Crashes whenever a guest submits preferences during pre-check-in.

hmp-reservation-service — guest-portal/guest-portal.controller.ts : 278​

  • STATUS: FIXED #7598 P1 (2026-06-08) — commits 1332388 + 919e320. xPrisma.outlet replaced with getRoomServiceMenu from @hmp/service-clients@1.28.3 (new pos internal endpoint 328469a); suite green.
  • Outlet.outlet (model not in local schema) — missing-relation-crash
  • Evidence: CrossServicePrisma = any; line 278 calls this.xPrisma.outlet.findFirst(...). outlet is not a model in prisma/schema.prisma (belongs to hmp-pos-service). At runtime this.prisma.outlet is undefined → TypeError: Cannot read properties of undefined.
  • Reachability: GET /guest-portal/menu/:propertyId → GuestPortalController.getRoomServiceMenu(). Public guest-facing endpoint for room service ordering. Crashes on every request.

hmp-reservation-service — guest-portal/guest-portal.controller.ts : 290​

  • STATUS: FIXED #7598 P1 (2026-06-08) — commits 1332388 + 919e320 (same as 278 above; getRoomServiceMenu returns structured menu data).
  • MenuCategory.menuCategory (model not in local schema) — missing-relation-crash
  • Evidence: CrossServicePrisma = any; line 290 calls this.xPrisma.menuCategory.findMany(...). menuCategory is not in prisma/schema.prisma (belongs to hmp-pos-service). Would crash even if outlet.findFirst somehow succeeded.
  • Reachability: GET /guest-portal/menu/:propertyId → GuestPortalController.getRoomServiceMenu(). Public guest-facing endpoint.

hmp-reservation-service — availability/availability.service.ts : 473​

  • STATUS: FIXED #7598 P1 (2026-06-08) — commit ff4942e. xPrisma.room.count replaced with ServiceClient; availability + overflow endpoints repaired; suite green.
  • Room.room (model not in local schema) — missing-relation-crash
  • Evidence: CrossServicePrisma = any; lines 473 and 476 call xPrisma3.room.count(...). room is not a model in prisma/schema.prisma (the NOTE at schema line 509 explicitly states Room was deleted). At runtime this.prisma.room is undefined → TypeError: Cannot read properties of undefined (reading 'count'). Both calls are in a Promise.all so either failure crashes the inventory calendar response.
  • Reachability: GET /inventory/:propertyId/:roomTypeId → AvailabilityService.getInventoryCalendar(). Publicly accessible. The tape-chart and availability calendar in the dashboard call this endpoint. Every inventory calendar request crashes.

hmp-reservation-service — cart/checkout.service.ts : 152​

  • STATUS: FIXED #7598 P1 (2026-06-08) — commit ff4942e. Property.roomTypes include replaced with ServiceClient; suite green.
  • Property.roomTypes (no @relation on local Property model) — missing-relation-crash
  • Evidence: Local Property model (schema lines 471-496) has: id, tenantId, code, name, type, starRating, addressLine1, addressLine2, city, state, country, postalCode, phone, email, website, timezone, currency, settings, isActive, createdAt, updatedAt, reservations — no roomTypes relation. Line 145 calls xPrisma.property.findMany({ include: { roomTypes: { ... } } }). Prisma throws 'Unknown field roomTypes for include statement on model Property' at runtime.
  • Reachability: GET /availability/overflow → CheckoutService.getOverflowSuggestions(). Staff-facing route for sister-property room suggestions when primary property is sold out. Crashes on every request.

hmp-reservation-service — routing/routing-rules.service.ts : 357​

  • STATUS: FIXED #7598 P1 (2026-06-08) — commit 9c04ecb. OTA routing consumer + DB-level dedup index; suite green.
  • RoomReservation.roomType (soft UUID ref — no @relation on RoomReservation) — missing-relation-crash
  • Evidence: Line 355-358: include: { guest: true, roomReservations: { include: { roomType: true } } } inside an xPrisma.$transaction. roomType is not a relation on the local RoomReservation model (schema lines 210-237, only roomTypeId String @db.Uuid). Prisma throws 'Unknown field roomType for include statement on model RoomReservation' at runtime.
  • Reachability: RoutingRulesService is a CHANNEL_BOOKING_RECEIVED event consumer (onModuleInit subscribes to the Kafka topic). Fires on every inbound OTA channel booking. Every OTA booking processed by the routing engine crashes inside the transaction after the reservation is created — the history record is never written and the event processing fails.

hmp-reservation-service — routing/routing-rules.service.ts : 362-371​

  • STATUS: FIXED #7598 P1 (2026-06-08) — commit 9c04ecb (same as above).
  • Folio.folioType, companyId, currency (fields not on local Folio model) — missing-relation-crash
  • Evidence: Local Folio model (schema lines 570-584) has only: id, reservationId, reservation, folioNumber, guestName, totalCharges, totalPayments, balance, status, createdAt, updatedAt. Line 362-371: tx.folio.create({ data: { folioType: ..., companyId: ..., currency: ... } }). None of folioType, companyId, currency exist on the local Folio model. Prisma throws P2019 'Unknown field folioType' at runtime inside the routing transaction.
  • Reachability: Same CHANNEL_BOOKING_RECEIVED consumer path as above. Even if the roomType include issue were fixed, this crash is the next barrier. Every OTA booking processed by the routing engine fails to create the folio.

hmp-reservation-service — reservations/multi-property-reservation.service.ts : 74​

  • STATUS: FIXED #7598 P1 (2026-06-08) — commit f4ba58e (same reservation-groups/waitlist/multi-property/availability/overflow batch). Suite green.
  • RatePlan.ratePlan (model deleted Phase 2 Sprint 1 — not in local schema) — missing-relation-crash
  • Evidence: CrossServicePrisma = any; line 74 calls tx.ratePlan.findFirst(...) inside an xPrisma.$transaction. ratePlan is not a model in prisma/schema.prisma. At runtime prisma.ratePlan is undefined → TypeError: Cannot read properties of undefined (reading 'findFirst').
  • Reachability: POST /reservations/multi-property → MultiPropertyReservationController → MultiPropertyReservationService.createMultiPropertyReservation(). Registered via ReservationsModule in AppModule. Crashes on every multi-property booking attempt.

hmp-reservation-service — reservations/multi-property-reservation.service.ts : 78​

  • STATUS: FIXED #7598 P1 (2026-06-08) — commit f4ba58e (same batch).
  • RoomType.roomType (model deleted Phase 2 Sprint 1 — not in local schema) — missing-relation-crash
  • Evidence: CrossServicePrisma = any; line 78 calls tx.roomType.findFirst(...) inside same xPrisma.$transaction. roomType is not in prisma/schema.prisma. Would crash even if ratePlan issue were fixed. Both calls are in the same transaction loop.
  • Reachability: POST /reservations/multi-property → MultiPropertyReservationService. Same path as above.

hmp-housekeeping-service — inventory/auto-reorder.service.ts : 353-357, 387-391​

  • STATUS: FIXED #7598 P1 (2026-06-08) — commit deed371. upsert + open-PO idempotency; suite green.
  • InventoryItem.N/A — findFirst/update on local model — other
  • Evidence: auto-reorder.service.ts uses this.prisma.inventoryItem.findFirst (lines 353, 387) and this.prisma.inventoryItem.update (line 357) — these are typed calls on a model that exists in the schema but whose table is always empty (same phantom InventoryItem issue as above). findFirst returns null → getCurrentStock returns 0 for every rule item, meaning every rule item perpetually appears 'below threshold' (currentStock=0 ≤ any reorderPoint). The daily cron (EVERY_DAY_AT_8AM, handleDailyReorderCheck) will draft a PurchaseOrder for every active ReorderRule on every run, creating unbounded DRAFT purchase orders. receivePurchaseOrder also silently no-ops the stock increment (inv=null path, line 361-363 logs a warning and continues). This is a logic defect rather than a crash, and the cron is the primary path.
  • Reachability: @Cron(EVERY_DAY_AT_8AM) AutoReorderService.handleDailyReorderCheck → checkAndReorder → getCurrentStock (all properties with isActive reorder rules). Also POST /inventory/reorder/check/:propertyId → manual trigger. PATCH /inventory/reorder/purchase-orders/:id/receive → receivePurchaseOrder (stock increment silently no-ops).

hmp-banquet-service — events/event-group.service.ts : 101-115​

  • STATUS: FIXED #7598 P1 (2026-06-08) — commits 5d9515d + c5f1516. Group.reservations phantom include removed; GDPR consumer dedup resolved; suite green.
  • Group.reservations — missing-relation-crash
  • Evidence: xPrisma (CrossServicePrisma = any) bypasses tsc. The local schema.prisma Group model declares only one relation: eventBookings EventBooking[]. There is no reservations field at all. Prisma will throw PrismaClientValidationError: Unknown field 'reservations' for include statement on model 'Group' at runtime on every call that reaches this branch (i.e. whenever event.groupId is non-null). Schema check: grep for 'reservations' in schema.prisma returns zero hits on the Group model — only eventBookings EventBooking[] exists.
  • Reachability: GET /api/events/:id/group → EventsController.getEventGroup() (line 147-151, events.controller.ts) → EventGroupService.getEventGroup() → this.xPrisma.group.findUnique({ include: { reservations: ... } }). Reached whenever any confirmed event has a linked groupId. Also reachable indirectly via EventGroupService.createGroupForEvent when groupId is already set (line 45 returns without the bad include, so that path is safe).

hmp-banquet-service — events/event-group.service.ts : 103-113​

  • STATUS: FIXED #7598 P1 (2026-06-08) — commits 5d9515d + c5f1516 (same as above).
  • Group.reservations.guestId — phantom-readmodel-relation
  • Evidence: Even if a reservations relation were added to the local Group schema, the banquet-service has zero consumers that write Reservation rows into a local reservations table. The only Group writers are EventGroupService.createGroupForEvent (create) and cancelGroupForEvent/updateRoomBlock (update) — all in the same file. No inbound message subscriber syncs reservation-service Reservation data into banquet's DB. The select: { ..., guestId: true } field would silently return null/empty arrays for every reservation join. Confirmed by exhaustive grep: only two consumers exist (both GuestErasedConsumer variants), neither touching Group or Reservation tables.
  • Reachability: Same path as above: GET /api/events/:id/group. The missing-relation-crash (finding 1) fires before the phantom-relation can manifest, but both defects exist at the same site.

hmp-property-service — edge/edge-snapshot.service.ts : 60-126​

  • STATUS: FIXED #7598 P1 (2026-06-08) — property edge/tenant-config/custom-fields/migration cluster (6 commits). xPrisma phantom models removed; suite green.
  • Outlet / MenuCategory / POSTable / HousekeepingTask.xPrisma.outlet, xPrisma.menuCategory (with nested include items.modifierGroups.modifiers), xPrisma.pOSTable, xPrisma.housekeepingTask — all absent from local schema — missing-relation-crash
  • Evidence: schema.prisma contains no 'Outlet', 'MenuCategory', 'POSTable', or 'HousekeepingTask' models. edge-snapshot.service.ts casts to CrossServicePrisma (any) and calls these four models inside a Promise.all. Any of the four will throw at runtime. The reservation query (line 60) IS in the local schema (Reservation model), but outlet, menuCategory, pOSTable, and housekeepingTask are owned by hmp-pos-service and hmp-housekeeping-service respectively.
  • Reachability: GET /edge/snapshot/:propertyId (EdgeController.generateSnapshot). Called by edge node bootstrapping when a kiosk comes online.

hmp-property-service — edge/edge-sync-consumer.service.ts : 106, 172-194, 197-229​

  • STATUS: FIXED #7598 P1 (2026-06-08) — property edge cluster (same 6-commit batch). AuditLog/HousekeepingTask/POSOrder phantom writes removed; suite green.
  • AuditLog / HousekeepingTask / POSOrder.prisma.auditLog (line 106), prisma.housekeepingTask (lines 172/186), prisma.pOSOrder (lines 201/221) — all absent from local schema — missing-relation-crash
  • Evidence: File has @ts-nocheck (line 1) to suppress all TypeScript errors. schema.prisma has no 'AuditLog', 'HousekeepingTask', or 'POSOrder' models. The service calls typed this.prisma.auditLog.create, this.prisma.housekeepingTask.create/update, and this.prisma.pOSOrder.upsert/update — none of which exist on the PrismaService instance for this schema. Every edge.sync.housekeeping or edge.sync.pos-orders Kafka message processed by reconcileHousekeeping or reconcilePosOrder will throw. The auditLog.create on line 106 is inside processMutation and runs for ALL entity types including the otherwise-safe reservations and rooms branches.
  • Reachability: Kafka consumer subscribed to topics: edge.sync.reservations, edge.sync.rooms, edge.sync.housekeeping, edge.sync.pos-orders, edge.sync.guests (EdgeSyncConsumerService.onModuleInit). Any edge.sync.* message arrival triggers processMutation which always ends with auditLog.create — so even the reservation/room reconcile paths crash at the audit-log step.

hmp-property-service — tenant-config/tenant-config.service.ts : 74, 111, 140, 196-211, 224​

  • STATUS: FIXED #7598 P1 (2026-06-08) — property tenant-config cluster (same 6-commit batch). TenantConfig model added; Tenant.parentTenantId migration added; suite green.
  • TenantConfig (tenantConfig) / Tenant.parentTenantId.xPrisma.tenantConfig (model absent from schema) + Tenant.parentTenantId (field absent from local Tenant read-model) — missing-relation-crash
  • Evidence: schema.prisma defines no 'TenantConfig' model and the local Tenant model (lines 65-77) has no 'parentTenantId' field. tenant-config.service.ts casts to (this.prisma as any).tenantConfig for all CRUD operations (lines 74/111/140/224) — each will throw 'Cannot find model tenantConfig'. Additionally, getHierarchy (line 196) selects { id: true, parentTenantId: true } from the local Tenant model — Prisma will reject this with 'Unknown field parentTenantId in select' since the local read-model only has id/name/slug/isActive/settings/createdAt/updatedAt.
  • Reachability: GET /tenant-config (getAll → getEffectiveConfig → getHierarchy crashes immediately), GET /tenant-config/:category (getConfig calls tenantConfig.findMany), PUT /tenant-config or PUT /tenant-config/:category/:key (setConfig → tenantConfig.upsert), DELETE /tenant-config/:category/:key (deleteConfig → tenantConfig.delete). Every tenant-config endpoint crashes.

hmp-property-service — custom-fields/custom-fields.service.ts : 47, 71, 101, 116, 129, 136​

  • STATUS: FIXED #7598 P1 (2026-06-08) — property custom-fields cluster (same 6-commit batch). CustomFieldDefinition model added; suite green.
  • CustomFieldDefinition.xPrisma.customFieldDefinition (model absent from local schema) — missing-relation-crash
  • Evidence: schema.prisma has no 'CustomFieldDefinition' model. custom-fields.service.ts casts to (this.prisma as any).customFieldDefinition for all operations (findMany, create, findUnique, update, soft-delete). Every call will throw at runtime.
  • Reachability: GET /custom-fields/definitions (getDefinitions), POST /custom-fields/definitions (createDefinition), PUT /custom-fields/definitions/:id (updateDefinition), DELETE /custom-fields/definitions/:id (deleteDefinition), POST /custom-fields/validate (validateCustomFields → getDefinitions). All custom-field endpoints crash.

hmp-property-service — migration/ota-remapper.service.ts : 137, 154, 163, 193​

  • STATUS: FIXED #7598 P1 (2026-06-08) — property migration cluster (same 6-commit batch). ChannelConnection xPrisma calls removed; suite green.
  • ChannelConnection.xPrisma.channelConnection (model absent from local schema) — missing-relation-crash
  • Evidence: schema.prisma has no 'ChannelConnection' model (owned by hmp-channel-service). ota-remapper.service.ts casts to (this.prisma as any).channelConnection for findUnique/update/create/findMany. Any call to applyMappings or listMappings crashes.
  • Reachability: POST /migration (MigrationController routes calling OtaRemapperService.applyMappings) and GET /migration (listMappings). Triggered during PMS data migration flows via OTA code remapping.

hmp-property-service — migration/migration.service.ts : 220-246, 284-312​

  • STATUS: FIXED #7598 P1 (2026-06-08) — property migration cluster (same 6-commit batch). Folio/MenuCategory/MenuItem xPrisma writes removed; suite green.
  • Folio / MenuCategory / MenuItem.xPrisma.folio, xPrisma.menuCategory, xPrisma.menuItem (all absent from local schema) — missing-relation-crash
  • Evidence: schema.prisma has no 'Folio', 'MenuCategory', or 'MenuItem' models (owned by hmp-frontdesk-service and hmp-pos-service). migration.service.ts casts to (this.prisma as any) for these three writes inside importFolios and importPosMenu.
  • Reachability: POST /migration (MigrationController.importFolios and importPosMenu endpoints). Triggered during legacy PMS data import for folio and POS menu migration.

hmp-property-service — migration/parallel-run.service.ts : 236-251​

  • STATUS: FIXED #7598 P1 (2026-06-08) — property migration cluster (same 6-commit batch). FolioTransaction xPrisma call removed; suite green.
  • FolioTransaction.xPrisma.folioTransaction (model absent from local schema) — missing-relation-crash
  • Evidence: schema.prisma has no 'FolioTransaction' model. parallel-run.service.ts casts to (this.prisma as any).folioTransaction.aggregate(...) inside compareWithLegacyPMS. The aggregate includes a nested folio.reservation.propertyId filter — even if the model existed, this double-nested include is non-trivial.
  • Reachability: POST /migration (parallel run comparison path — compareWithLegacyPMS called by ParallelRunService). Migration endpoint for parallel-run comparison feature.

hmp-property-service — migration/migration-import.service.ts : 803, 1131​

  • STATUS: FIXED #7598 P1 (2026-06-08) — property migration cluster (same 6-commit batch). ImportJob model added; persistJobToDb/getJobFromDb no longer no-ops; suite green.
  • ImportJob.xPrisma.importJob (model absent from local schema) — missing-relation-crash
  • Evidence: schema.prisma has no 'ImportJob' model. The source comment at line 69 explicitly acknowledges this: 'importJob model is not in the property-service Prisma schema yet. Cast to any so the persistence helpers compile while we decide whether to add the model or move the durable job state to a different service. Without the model, persistJobToDb / getJobFromDb are effectively best-effort no-ops at runtime.' The xPrisma.importJob.upsert (line 803) and findUnique (line 1131) will throw on every invocation.
  • Reachability: POST /migration/import/:jobId/import (persistJobToDb called after each phase) and GET /migration/import/:jobId (getJobFromDb to supplement in-memory state). Every import job status persistence and durable-read operation fails.

hmp-crm-service — campaigns/campaigns.service.ts : 494-508​

  • STATUS: FIXED #7598 P1 (2026-06-08) — commit 3a90f41. lastStayDate filter removed; suite green.
  • Guest.lastStayDate — missing-relation-crash
  • Evidence: segmentRecipients() builds a where clause as Record<string,unknown> and conditionally sets where.lastStayDate = { lt: ... } (line 496) and where.lastStayDate = { gt: ... } (line 504) when segment rules include lastStayBefore / lastStayAfter. This object is then cast to Prisma.GuestWhereInput at line 525 and passed to prisma.guest.findMany(). The local Guest read-model (schema.prisma lines 831-855) has no lastStayDate field — it has only: id, tenantId, propertyId, firstName, lastName, email, phone, vipLevel, guestType, totalStays, marketingConsent, dateOfBirth, anniversary, loyaltyMemberId, createdAt, updatedAt. Prisma will throw PrismaClientValidationError: Unknown field 'lastStayDate' for type GuestWhereInput at runtime on the first request where a campaign has lastStayBefore or lastStayAfter in segmentRules.
  • Reachability: GET /api/campaigns/:id/recipients (CampaignsController.getRecipients → segmentRecipients) and POST /api/campaigns/:id/execute (CampaignsController.executeCampaign → segmentRecipients → prisma.guest.findMany with the bad where). Triggered whenever any campaign has segmentRules.lastStayBefore or segmentRules.lastStayAfter set.

hmp-notification-service — channels/whatsapp-catalog.service.ts : 99-116​

  • STATUS: FIXED #7598 P1 Finding 1 (2026-06-07). Added propertyId String? @db.Uuid + @@index([propertyId]) to local MenuCategory schema (idempotent migration 20260607000000_menu_category_property_id). Fixed filter to category: { propertyId } scalar. Added MenuSyncConsumer (Finding 2) to populate the column. WARN log added when catalog is empty. 4 new spec assertions. See notification-service CLAUDE.md Gotcha.
  • MenuCategory.outlet (nested where relation filter) — missing-relation-crash
  • Evidence: getMenuCatalog() constructs where = { isActive: true, category: { outlet: { propertyId }, ... } } and passes it to this.xPrisma.menuItem.findMany({ where, include: { category: true } }). MenuItem.category is a valid @relation to MenuCategory (schema lines 168-193). However MenuCategory has only outletId String @db.Uuid as a plain scalar — there is NO outlet @relation on MenuCategory. Prisma rejects the nested relation filter 'outlet' at runtime with PrismaClientValidationError: 'Unknown field outlet in where argument'. The include: { category: true } is valid (relation exists). There is no try/catch in getMenuCatalog() or its direct callers, so the Prisma error propagates up to the NestJS exception filter and returns HTTP 500 to the caller.
  • Reachability: GET /api/whatsapp/catalog/:propertyId — exposed via WhatsAppWebhookController.getMenuCatalog() at src/channels/whatsapp-webhook.controller.ts line 125-130. Also called internally by WhatsAppCatalogService.sendMenuCatalog() which is invoked during WhatsApp room-service flows.
  • MenuSyncConsumer (Finding 2) also ADDED — src/read-model-sync/menu-sync.consumer.ts subscribes to pos.menu.changed, fetches GET /api/outlets/:id/menu from pos-service (INTERNAL_SERVICE_TOKEN), upserts MenuCategory (with propertyId) + MenuItem rows idempotently. Best-effort. 6 specs. Follow-up #7599: add PosServiceClient.getOutletMenu() to @hmp/service-clients.

hmp-notification-service — dispatcher/tenant-lifecycle.consumer.ts : 156-173​

  • STATUS: FIXED #7598 P1 Finding 3 (2026-06-07). Removed billingContactName/billingContactEmail from SELECT (not in local Tenant schema). Now uses supportEmail (exists locally) as the single contact email, brandName || name || 'Tenant Admin' as display name. All 4 lifecycle emails now deliver. 2 new schema-safety specs + existing mocks corrected. See notification-service CLAUDE.md Gotcha.
  • Tenant.billingContactName, billingContactEmail (select fields) — missing-relation-crash
  • Evidence: TenantLifecycleConsumer.resolveContact() casts this.prisma as unknown as { tenant: { findUnique: ... } } and calls tenant.findUnique({ where: { id: tenantId }, select: { name, billingContactName, billingContactEmail, supportEmail } }). The local Tenant model (schema lines 260-270) has only: id, name, brandName, supportEmail, customDomain, createdAt, updatedAt. Fields billingContactName and billingContactEmail do NOT exist in the local schema. Prisma throws PrismaClientValidationError: 'Unknown field billingContactName in select'. The try/catch at line 182 catches the error, logs WARN, and returns null. resolveContact returning null causes the send() method (line 125-130) to log WARN and skip email dispatch. Result: all four tenant lifecycle emails — tenant.activated, tenant.suspended, tenant.cancelled, tenant.pending_payment — silently fail to deliver. This affects the dunning/billing notification path and tenant onboarding.
  • Reachability: Kafka consumer subscriptions on 'tenant.activated', 'tenant.suspended', 'tenant.cancelled', 'tenant.pending_payment' events emitted by hmp-auth-service TenantsService state machine. Handlers: TenantLifecycleConsumer.onActivated/onSuspended/onCancelled/onPendingPayment.

hmp-spa-service — activities/activities.service.ts : 71-78​

  • ActivityBooking.guest — phantom-readmodel-relation
  • Evidence: findOne() calls activityBookingModel.findFirst({ include: { bookings: { include: { guest: true } } } }) (via the casted (this.prisma as any).activity path). ActivityBooking.guest is defined in schema.prisma line 246 as 'guest Guest? @relation(fields: [guestId], references: [id])'. The Guest model is explicitly labelled a cross-service read model (schema comment line 261-263), but no consumer in hmp-spa-service populates the guests table (only GuestErasedConsumer.updateMany exists). All joined guest fields will be null at runtime, degrading the activity detail response silently.
  • Reachability: GET /api/spa/activities/:id → ActivitiesController.findOne() → ActivitiesService.findOne()

hmp-spa-service — activities/activities.service.ts : 291-301​

  • ActivityBooking.guest — phantom-readmodel-relation
  • Evidence: listBookings() calls activityBookingModel.findMany({ include: { activity: true, guest: true } }) on the casted activityBooking delegate. Same root cause: Guest read model table has no writer in hmp-spa-service. guest field on every returned ActivityBooking row is always null. Sibling of the findOne finding.
  • Reachability: GET /api/spa/activities/bookings → ActivitiesController.listBookings() → ActivitiesService.listBookings()

hmp-spa-service — activities/activities.service.ts : 266-273​

  • ActivityBooking.guest — phantom-readmodel-relation
  • Evidence: cancelBooking() calls activityBookingModel.update({ include: { activity: true, guest: true } }) on the casted delegate. Same root cause as listBookings/book includes. guest field always null.
  • Reachability: POST /api/spa/activities/bookings/:bookingId/cancel → ActivitiesController.cancelBooking() → ActivitiesService.cancelBooking()

hmp-spa-service — appointments/appointments.service.ts : 165-173​

  • Guest.N/A (guard read on read model) — phantom-readmodel-relation
  • Evidence: appointments.service.ts create() performs this.prisma.guest.findFirst({ where: { id: dto.guestId, tenantId } }) (typed, compile-safe) and throws NotFoundException if no row exists. Because the Guest table has no writer in hmp-spa-service (confirmed above), every POST /spa/appointments with a guestId will always hit the NotFoundException gate, making linked-guest appointment booking permanently broken. This is not a crash but a silent feature break on the guestId path.
  • Reachability: POST /api/spa/appointments → AppointmentsController.create() → AppointmentsService.create()

P2 (17)​

hmp-reservation-service — reservations/reservations.service.ts : 2205​

  • Company.company (relation exists in schema but Company table is never populated by any sync consumer) — phantom-readmodel-relation
  • Evidence: Company model IS defined in schema (lines 457-469) with a valid Reservation.company @relation. HOWEVER, grep of the entire src/ tree finds zero writes (upsert/create/update/createMany) to the Company table — ReadModelSyncModule only syncs Guest and Property. Line 2200 is a TYPED (non-casted) this.prisma.reservation.findFirst with include: { company: true }. The join executes without error but always returns company=null for every reservation (no data to join). Staff on the reservation detail page never see the company name even when companyId is set.
  • Reachability: GET /reservations/:id → ReservationsService.findOne(). The primary reservation detail endpoint used by frontdesk, dashboard tape chart, and everywhere else. Silent null — no crash, but the company name is always absent on any reservation with a companyId.
  • STATUS (2026-06-08): DOCUMENTED 22f5520 + DESIGNED — no publisher exists for company.* events (EventTopics enum has no company entries; frontdesk POST /companies HTTP CRUD never emits events; guest-service has no company controller). Decision: do NOT add a dead consumer. The typed include is safe; data-path gap documented in reservation-service CLAUDE.md Gotcha #47. Publisher gap is now DESIGNED: hmp-docs/docs/superpowers/specs/2026-06-08-company-ownership-and-events-design.md defines company.created / company.updated / company.deleted topics and the ownership model. Security fix: #7610 (company CRUD mass-assignment) Closed-Verified (2026-06-08) — CreateCompanyDto/UpdateCompanyDto whitelist added, blocking unintended field injection on company create/update.

hmp-pos-service — menu/menu.service.ts : 565​

  • Recipe.yieldQuantity — suspicious-cast
  • Evidence: Schema check: Recipe model (prisma/schema.prisma:515) declares the field as yield Decimal, not yieldQuantity. The code is cast via (this.prisma as any).recipe.findUnique(...) which bypasses tsc. At line 565 the response is built as yieldQuantity: Number(recipe.yieldQuantity ?? 1) — recipe.yieldQuantity resolves to undefined at runtime because the column does not exist, so the ?? 1 fallback silently returns 1 on every call regardless of the actual stored yield quantity. This does not crash (no Prisma 'Unknown field' error because the field is read from the JS object, not passed in a select/include), but every banquet-costing recipe lookup returns yield=1 instead of the persisted value, making all recipe cost computations wrong.
  • Reachability: GET /menu/items/:id/recipe → MenuController.getRecipeByMenuItem (menu.controller.ts:117-124). Class-level @RequiresFeature('pos.basic'); no additional @Roles so any JWT-authenticated user with the pos.basic feature flag can reach it. Also consumed cross-service by banquet-service for recipe costing.
  • STATUS (2026-06-08): FIXED 626ced4+8bd9041 — schema field is yield; costing was always falling back to 1; read recipe.yield directly, dropped (this.prisma as any) cast. 3 new specs (real yield value, NotFoundException on missing recipe, ingredient field mapping). Suite 435 green. Build 32394 pushed. CLAUDE.md gotcha #33.

hmp-banquet-service — consumers/guest-erased.consumer.ts : 35-91​

  • EventBooking.N/A — other
  • Evidence: Duplicate GuestErasedConsumer registration: src/consumers/guest-erased.consumer.ts (the stub / ACK-only implementation from AP-T08-005, registered in AppModule line 108) AND src/events/consumers/guest-erased.consumer.ts (the newer email-scrub implementation, registered in EventsModule.providers line 15). Both call kafka.subscribe(EventTopics.GUEST_ERASED, ...) in onModuleInit. Depending on how the KafkaService registers consumer-group offsets, this either creates a double-delivery (each event processed twice, with two different behaviours — the stub does a no-op empty $transaction + ACK, the newer one scrubs clientEmail) or a consumer-group conflict. The stub at src/consumers/ predates the scrub implementation at src/events/consumers/ and should be removed.
  • Reachability: Both fire on every guest.erased event received from the message bus. AppModule (app-level) and EventsModule (module-level) both instantiate their own GuestErasedConsumer via NestJS DI, both registering at onModuleInit.
  • STATUS (2026-06-08): ALREADY-FIXED c5f1516 (P1 wave). src/consumers/ directory is completely GONE from hmp-banquet-service. Only src/events/consumers/guest-erased.consumer.ts remains. The duplicate stub was removed as part of #7598 P1 banquet work. Banquet CLAUDE.md gotcha #18 confirms this.

hmp-guest-service — ai-summary/ai-summary.service.ts : 55-67​

  • RoomReservation.ratePlan — phantom-readmodel-relation
  • Evidence: ai-summary.service.ts lines 55-67 include reservations.roomReservations.ratePlan and reservations.folios.transactions via typed this.prisma.guest.findFirst. The schema defines RoomReservation.ratePlan RatePlan? (schema.prisma line 557) and Folio.transactions FolioTransaction[] (line 610) so the include is schema-valid and does not throw. However the read-model-sync service (read-model-sync.service.ts) only upserts Property and Reservation rows — NO writer exists for rate_plans, room_reservations, folios, or folio_transactions tables anywhere in this codebase (confirmed by grep across all .ts files excluding specs). tenant-export.service.ts explicitly labels these "cross-service read model stubs". Every join yields ratePlan: null and transactions: [] at runtime. The AI summary's preferredMealPlan computation (line 112: rr.ratePlan?.mealPlan || 'EP') always falls back to 'EP'; folio spend is always zero.
  • Reachability: GET /guests/:id/ai-summary (AiSummaryController, Roles SUPER_ADMIN/ADMIN/MANAGER/FRONT_DESK)
  • STATUS (2026-06-08): FIXED af2a782 — read-time hydration via PropertyServiceClient.getRoomTypes / PropertyServiceClient.getRatePlan + FolioServiceClient.getFoliosByReservation. Phantom ratePlan + transactions includes removed; real data fetched at query-time via ServiceClients. LIVE: GET /guests/:id/ai-summary 200 (was phantom-include crash). Follow-up: FeedbackSurvey hydration — survey: null + WARN emitted; crm-service owns FeedbackSurvey and has no published ServiceClient method yet.

hmp-guest-service — guests/guests.service.ts : 1033-1048​

  • RoomReservation.roomType — phantom-readmodel-relation
  • Evidence: get360View (guests.service.ts line 1033) includes reservations.roomReservations.roomType and feedbackResponses.survey via typed this.prisma.guest.findFirst. Schema relations exist: RoomReservation.roomType RoomType (schema.prisma line 556) and FeedbackResponse.survey FeedbackSurvey (line 652). But room_types, room_reservations, feedback_surveys, and feedback_responses tables are never populated in this service — no sync consumer writes to them (confirmed by exhaustive grep; tenant-export.service.ts confirms stubs). All these joins silently return empty arrays. The 360-view response shows no stay room-type detail and no feedback history for any live guest.
  • Reachability: GET /guests/:id/360 (GuestsController.get360View, Roles SUPER_ADMIN/ADMIN/MANAGER/FRONT_DESK)
  • STATUS (2026-06-08): FIXED af2a782 — read-time hydration via PropertyServiceClient.getRoomTypes + FolioServiceClient.getFoliosByReservation. Phantom roomType + feedbackResponses.survey includes removed; ServiceClient reads injected. LIVE: GET /guests/:id/360 200 (was phantom-include crash). Follow-up: FeedbackSurvey hydration still returns survey: null + WARN — same root cause as P2-4 (crm-service owns, no client method yet).

hmp-guest-service — guests/guests.service.ts : 955-961​

  • RoomReservation.roomType — phantom-readmodel-relation
  • Evidence: getGuestHistory (guests.service.ts line 955) includes reservations.roomReservations.roomType via xPrisma (typed any). Schema relation RoomReservation.roomType exists (schema.prisma line 556) so no crash. The room_reservations and room_types tables are never written to by any consumer in this service; every roomReservations array in the history response is always empty. The US-004-001 fix comment at line 944 already removed the room relation crash but the roomType include still joins an always-empty cross-service stub.
  • Reachability: GET /guests/:id/history (GuestsController, Roles SUPER_ADMIN/ADMIN/MANAGER/FRONT_DESK/RESERVATIONS)
  • STATUS (2026-06-08): FIXED af2a782 — read-time hydration via PropertyServiceClient.getRoomTypes + FolioServiceClient.getFoliosByReservation. Phantom roomType include removed; ServiceClient reads injected. LIVE: GET /guests/:id/history 200 (was phantom-include crash).

hmp-guest-service — gdpr/gdpr.service.ts : 251​

  • STATUS: FIXED #7598 P2-7 (2026-06-08). Option B (read-time hydration via FolioServiceClient.getFoliosByReservation) chosen — complete, no backfill problem. GdprModule imports ServiceClientsModule.register(); GdprService injects @Optional() folioClient?. Dead folios: true Prisma include removed. Each reservation gets folios: FolioDto[] on success or financialDataUnavailable: true on failure — never silent empty array. 21 tests green (8 new for the folio hydration, 13 existing). See guest-service CLAUDE.md Security/Configuration section.
  • Folio.folios — phantom-readmodel-relation
  • Evidence: exportGuestData (gdpr.service.ts line 251) includes reservations: { include: { folios: true } } via typed this.prisma.guest.findUnique. Schema relation Reservation.folios Folio[] exists (schema.prisma line 539). The folios table is never written to by any sync consumer in guest-service (confirmed by grep; tenant-export.service.ts labels it a stub). Every GDPR Art.20 data portability export returns reservations[].folios: [] — the exported data omits all financial transactions, which is a compliance gap for GDPR portability.
  • Reachability: GET /gdpr/guests/:id/export (GdprController.exportGuestData, Roles SUPER_ADMIN/ADMIN) AND PATCH /dpdp/requests/:id/status when requestType=ACCESS (DpdpService.processRequest → GdprService.exportGuestData)

hmp-property-service — content/content.service.ts : 51, 63, 93, 103, 125, 138, 152, 158​

  • PropertyContent / PropertyPhoto.xPrisma.propertyContent, xPrisma.propertyPhoto (models absent from local schema) — missing-relation-crash
  • Evidence: schema.prisma has no 'PropertyContent' or 'PropertyPhoto' model. content.service.ts source comment at line 38 explicitly acknowledges: 'propertyContent and propertyPhoto models are not yet in the property-service Prisma schema. At runtime these calls will throw until the models are added — content management was a deferred Phase 5 feature with no production callers.'
  • Reachability: GET /content/:propertyId (getContent), PUT /content/:propertyId/:section (updateSection), GET /content/:propertyId/photos (getPhotos), POST /content/:propertyId/photos (addPhoto), PUT /content/:propertyId/photos/reorder (reorderPhotos), DELETE /content/photos/:photoId (deletePhoto). Also schema-org.service.ts lines 35/46/54 via GET /content/:propertyId/schema-org. Described as 'no production callers' but endpoints are live and routable.
  • STATUS (2026-06-08): FIXED 9681d20 — real PropertyContent + PropertyPhoto models added with idempotent migration 20260708200000; content.service.ts rewritten to typed Prisma (no any cast); verdict reachable and consumed (dashboard settings/website page). Suite 1057 green. LIVE: GET /properties/:id/content → 200 (was 500-crash); GET /properties/:id/photos → 200 (was 500-crash).

hmp-property-service — config-inheritance/config-inheritance.service.ts : 78, 82​

  • Tenant.property.tenant.brandName, property.tenant.logoUrl, property.tenant.primaryColor (fields absent from local Tenant read-model) — phantom-readmodel-relation
  • Evidence: Local Tenant read-model (schema.prisma lines 65-77) has only: id, name, slug, isActive, settings, createdAt, updatedAt. getEffectiveBranding() does include: { brand: true, tenant: true } then reads property.tenant.brandName, property.tenant.logoUrl, property.tenant.primaryColor (lines 78-82 of config-inheritance.service.ts) — all three are undefined (not a crash, but the branding merge always contributes null for those keys). The auth-service Tenant model has these fields but the property-service read-model never syncs them. ReadModelSyncService.handleTenantCreated (read-model-sync.service.ts) only writes id/name/slug/isActive — not the three branding fields.
  • Reachability: GET /properties/:propertyId/effective-branding (ConfigInheritanceController.getEffectiveBranding). Also invalidateBrandProperties is called on brand PUT. Silent degradation — returns null for those keys.
  • STATUS (2026-06-08): FIXED 298fbdc — brandName resolved from tenant.name (best local source, avoids phantom column); logoUrl and primaryColor returned as explicit null (not undefined). Auth-service TENANT_* events carry no branding fields — FU-86 follow-up: add brandName/logoUrl/primaryColor to event payload + corresponding sync columns in property-service read-model. Suite 1064 green.

hmp-channel-service — parity/parity.service.ts : 339​

  • RoomType.baseRate (read via xPrisma.roomType.findMany) — phantom-readmodel-relation
  • Evidence: Schema has model RoomType (room_types table) as a cross-service read-model. read-model-sync.service.ts only subscribes to RATE_PLAN_UPDATED and RESERVATION_CREATED — no consumer writes to room_types. No infrastructure seed targets hmp_channel.room_types (seed-resavenue-sandbox-property.sql targets hmp_property). Result: room_types is always empty in production channel-service DB. fetchBaseRates() returns an empty Map, so every call to checkParity() computes all baseRate fallbacks as 5000 (hardcoded at line ~225). Rate parity violations are calculated against a fictitious base rate, not the actual hotel rate. The select fields (id, baseRate) do exist on the RoomType model so the query does not throw — it silently returns [].
  • Reachability: GET /channel-sync/parity (ParityController.checkParity), GET /channel-sync/parity/check, GET /channel-sync/parity/history indirectly. Also reachable from the scheduled setInterval parity check wired in onModuleInit (fires every 6 hours when PARITY_SCHEDULED_CHECK_ENABLED != false) — this covers every property with an active ChannelConnection.
  • STATUS (2026-06-08): FIXED 81282e5 — PropertyServiceClient.getRoomTypes is now the authoritative source (no publisher exists for room-type events — room-types.service.ts has zero event publishes; EventTopics enum has no ROOM_TYPE_* entries). The fictitious ₹5000 fallback removed; parity now emits skip+WARN when base rate unavailable (a wrong verdict is worse than none). Feeds fail-soft to UUID+WARN for unknown room type codes. Suite 931 green.

hmp-channel-service — reporting-feeds/reporting-feeds.service.ts : 46​

  • RoomType.baseRate/code (read via (this.prisma as any).roomType.findMany) — phantom-readmodel-relation
  • Evidence: Same no-writer root cause as parity.service.ts. room_types table in channel DB is always empty. getRateParity() builds roomTypeMap from this query — when the map is empty, every allocation row maps roomTypeId to itself (the raw UUID) as the roomTypeCode. The RateParityFeedItem.roomTypeCode field carries a raw UUID instead of a human-readable code (e.g. STD-DBL). analytics-service LiveProxyResolver consumes this feed; downstream dashboard parity display shows UUID strings instead of room codes. The select { id, baseRate } and the where { propertyId } are valid fields on the RoomType schema, so no crash — just silent data degradation.
  • Reachability: GET /reporting-feeds/channel/rate-parity?propertyId=&date=&roomTypeId= consumed by analytics-service LiveProxyResolver (INTERNAL_SERVICE role, cross-service HTTP via service-account JWT). Reachable whenever analytics queries channel rate parity feed.
  • STATUS (2026-06-08): FIXED 81282e5 — same fix as P2-10; getRoomServiceMenu via PropertyServiceClient replaces phantom room_types lookup; UUID fallback emits WARN. Suite 931 green (covered by same batch commit).

hmp-crm-service — revenue/revenue.service.ts : 42-57​

  • Room.Room (phantom read model — no writer) — phantom-readmodel-relation
  • Evidence: Room model exists in schema.prisma (lines 897-911) as a cross-service read model. ReadModelSyncService (src/read-model-sync/read-model-sync.service.ts) only subscribes to GUEST_CREATED, GUEST_UPDATED, RESERVATION_CREATED — no room sync event. No other file in src/ calls prisma.room.create / upsert / createMany. Therefore the rooms table is always empty. revenue.service.ts:42 calls prisma.room.count() to get totalRooms — always returns 0. occupancyPercent is always 0/0 = 0 for calculateDynamicRate(). The same prisma.room.count() appears at line 134 in getForecast(), making the occupancy-based forecast always show 0 total rooms. Revenue recommendations (getRecommendations) calls getForecast() and will always recommend REDUCE_RATE (0% occupancy < 30% threshold).
  • Reachability: GET /api/revenue/dynamic-pricing/calculate → RevenueController.calculateDynamicRate → revenueService.getForecast + dynamicPricing.calculatePrice. GET /api/revenue/forecast. GET /api/revenue/recommendations. POST /api/revenue/dynamic-pricing/apply → dynamicPricing.applyDynamicRates (also uses raw SQL against room_reservations at line 406). GET /api/revenue/dynamic-pricing/preview.
  • STATUS (2026-06-08): FIXED a1ef9b8 — PropertyServiceClient.getActiveRoomCount + ReservationServiceClient.searchReservations replace phantom prisma.room.count + prisma.roomReservation.count. Honest INSUFFICIENT_DATA state emitted when property has no reservations on record (better than fabricated REDUCE_RATE). LIVE: GET /revenue/recommendations → { status: "INSUFFICIENT_DATA" } (was fabricated REDUCE_RATE from 0% phantom occupancy). Follow-up: reservation-service needs a property-occupancy-range endpoint for multi-day forecast.

hmp-crm-service — revenue/revenue.service.ts : 46-53​

  • RoomReservation.RoomReservation + RoomReservation.reservation (phantom read model — no writer) — phantom-readmodel-relation
  • Evidence: RoomReservation model exists in schema.prisma (lines 913-928) with a relation to Reservation (line 917). ReadModelSyncService does not subscribe to any room-reservation event and has no write path for RoomReservation. No file in src/ calls prisma.roomReservation.create / upsert / createMany. Therefore room_reservations table is always empty. revenue.service.ts:46 calls prisma.roomReservation.count({ where: { ..., reservation: { propertyId, tenantId } } }) — the reservation nested filter is a typed Prisma relation filter (relation exists in schema) but the table is always empty so count always returns 0. Additionally dynamic-pricing.service.ts lines 406 and 607 run raw SQL LEFT JOINs against room_reservations — also always yields 0 occupied rooms. All occupancy-based revenue calculations are permanently 0%.
  • Reachability: GET /api/revenue/dynamic-pricing/calculate, GET /api/revenue/forecast, GET /api/revenue/recommendations, POST /api/revenue/dynamic-pricing/apply, GET /api/revenue/dynamic-pricing/preview. Also AutomationService.processPreArrivalNotifications cron (daily at 09:00 via @Cron) calls prisma.reservation.findMany — this is correctly populated, but getForecast/calculateDynamicRate from the revenue paths use the empty RoomReservation table.
  • STATUS (2026-06-08): FIXED a1ef9b8 — same fix as P2-12; ReservationServiceClient.searchReservations replaces phantom prisma.roomReservation.count; raw SQL LEFT JOINs against empty room_reservations removed. Occupancy calculation now driven by live reservation-service data.

hmp-crm-service — campaigns/drip-engine.service.ts : 213-231​

  • GuestCommunication.GuestCommunication (phantom read model — no writer) — phantom-readmodel-relation
  • Evidence: GuestCommunication model exists in schema.prisma (lines 857-876). ReadModelSyncService does not subscribe to any notification-delivered/opened/clicked event and has no write path for GuestCommunication. Searching all src/ files finds only two references to guestCommunication: the guest-erased consumer (updateMany to scrub PII — scrubs nothing since table is empty) and drip-engine.service.ts line 213 which does prisma.guestCommunication.findMany() to evaluate step conditions IF_OPENED, IF_NOT_OPENED, IF_CLICKED. With the table always empty, lastComm is always null. evaluateStepCondition() then returns: condition=IF_NOT_OPENED → true (step fires even if guest opened), condition=IF_OPENED → false (step is permanently skipped), condition=IF_CLICKED → false (step is permanently skipped). Drip steps with IF_OPENED or IF_CLICKED conditions will never execute; IF_NOT_OPENED steps will always fire regardless of actual open status.
  • Reachability: POST /api/campaigns/:id/execute (campaigns.service.ts executeCampaign → dripEngineService.startDrip → BullMQ jobs → DripEngineService.processJob → evaluateStepCondition). BullMQ worker runs continuously; every drip job with an IF_OPENED or IF_CLICKED condition is permanently skipped. The crm-drip-campaigns BullMQ queue is the consumer.
  • STATUS (2026-06-08): DESIGNED — spec hmp-docs/docs/superpowers/specs/2026-06-08-notification-engagement-events-design.md (commit 89bccc9) defines the full publisher → EventTopics entries (notification.delivered / notification.opened / notification.clicked) → GuestCommunicationSyncConsumer → drip evaluator repair path. No writer will be added until the spec is operator-approved (engagement webhook infrastructure not yet present). Tracked as batch-10 design item.

hmp-notification-service — channels/email.service.ts : 301-313, 685-702​

  • Tenant.tenants table (phantom read-model — no writer) — phantom-readmodel-relation
  • Evidence: The local Tenant model is declared in schema.prisma (lines 260-270, mapped to 'tenants') and its table is created by migrations, but NO service or consumer ever writes to it. ReadModelSyncService subscribes only to guest.created/guest.updated events and writes to the Guest table. TenantReadModelConsumer writes to TenantReadModel (tenant_read_model table), NOT tenants. No seed script populates tenants. Therefore this.prisma.tenant.findUnique() always returns null at runtime. email.service.ts reads brandName (line 305) and supportEmail/brandName/customDomain (line 687) from this table for From-name and fallback-From-email derivation. Returning null on every call means every outbound email uses the hardcoded fallback 'Hotel Management' as sender display name, ignoring tenant branding. dispatcher.service.ts loadTenant() (line 551) also reads this table and the caller accesses tenant?.billingContactEmail which is additionally not in the local schema (returns undefined rather than throwing since no select clause is used). The channel services degrade silently rather than crash.
  • Reachability: email.service.ts legacySend() and sendV2() are invoked on every outbound email (confirmation letters, welcome emails, night audit reports, payment links, lifecycle emails). dispatcher.service.ts loadTenant() is called for TENANT_CREATED and DUNNING_REMINDER event handling.
  • STATUS (2026-06-08): FIXED 2a0388e — both email.service.ts reads now use tenantReadModel (PK tenantId; populated by TenantReadModelConsumer), not tenant (always-empty tenants table). Spec uses non-permissive mock (no tenant accessor). EP-014 failure VERIFIED pre-existing at parent commit 852de67 (regression guard). Suite 1064 green. Builds 32396+32401 deployed to test.

hmp-notification-service — channels/whatsapp-catalog.service.ts : 111-116​

  • MenuItem.MenuItem and MenuCategory (phantom read-models — no writer) — phantom-readmodel-relation
  • Evidence: MenuItem and MenuCategory are declared in schema.prisma (lines 168-193) and their tables are created in migration 20260526130000_schema_catchup_fu_77/migration.sql. However no consumer, sync service, or seed script writes to menu_items or menu_categories in hmp-notification-service. ReadModelSyncService only syncs guest rows. There is no menu sync consumer. The tables are always empty. Even if the runtime crash from the missing 'outlet' relation (separate P1 finding) were fixed, getMenuCatalog() would return an empty categories array for every property. This finding is secondary to the 'outlet' relation crash but documents that the underlying read-model has no data path.
  • Reachability: GET /api/whatsapp/catalog/:propertyId and WhatsApp room-service flows. Functionally moot until the P1 outlet-relation crash is fixed.
  • STATUS (2026-06-08): CORRECTION — the audit's OPEN verdict was STALE. FIXED 4fd6bc9+852de67: 4fd6bc9 added MenuSyncConsumer (pos.menu.changed → MenuCategory/MenuItem upserts in notification-service DB). 852de67 rewired getMenuCatalog onto PosServiceClient.getRoomServiceMenu (@hmp/service-clients@1.28.3) as the live-query path. Rows populate on pos.menu.changed events going forward (no backfill for pre-existing menus). The P1 outlet-relation crash is separately fixed.

hmp-asset-service — amc/amc-contracts.service.ts : 101​

  • AmcContractAsset.addedAt — phantom-readmodel-relation
  • Evidence: amc-contracts.service.ts:101 maps include: { assets: true } result to { assetId: a.assetId, addedAt: a.addedAt }. Schema check: AmcContractAsset has exactly three fields — id, amcContractId, assetId. There is NO addedAt column. The field was never added to prisma/schema.prisma. The amcContractAsset.create at line 83 only writes { amcContractId, assetId } confirming no addedAt has ever been populated. At runtime every element in the assets array returns addedAt: undefined. The dashboard at hmp-web-dashboard/src/app/(dashboard)/amc-contracts/[id]/page.tsx:101 guards with a.addedAt && !isNaN(new Date(a.addedAt).getTime()) so no crash occurs — the '(linked )' text is simply never rendered.
  • Reachability: GET /api/amc-contracts/:id → AmcContractsController.findOne → AmcContractsService.findOne (include: { assets: true }) → toResponse → every element of assets[].addedAt is undefined. Also exposed via GET /api/asset-registry/metrics (expiringAmcs) but that path does not include assets relation. The phantom field appears only on the detail endpoint.
  • STATUS (2026-06-08): FIXED 6a282b8 — dropped addedAt from AmcContractResponseDto.assets (now Array<{ assetId: string }>) and from toResponse mapper. Updated existing spec mock; 2 new specs assert field absent + element equals { assetId } exactly. CLAUDE.md gotcha #27. Suite execution gate in progress — noting 'pending suite gate' until build confirms green.

info (2)​

hmp-housekeeping-service — consumers/guest-erased.consumer.ts : 67​

  • HousekeepingTask.N/A — plain model access — suspicious-cast
  • Evidence: (this.prisma as any).housekeepingTask.updateMany is a casted call on a model that IS present in the local schema. The cast is unnecessary (PrismaService exposes housekeepingTask natively) but does not cause a runtime error — HousekeepingTask exists and updateMany will succeed. Severity is info because the data: {} no-op update and the model existence make this a style/maintenance issue, not a crash.
  • Reachability: KafkaService subscriber on topic 'guest.erased' — GDPR erasure compliance path.

hmp-auth-service — onboarding/seed-defaults.service.ts : 44-368​

  • SeedDefaultsService.tx.roomType / tx.ratePlan / tx.cancellationPolicy / tx.outlet / tx.property — other
  • Evidence: SeedDefaultsService casts the Prisma transaction to CrossServiceTx = any and accesses tx.roomType, tx.ratePlan, tx.cancellationPolicy, tx.outlet, and tx.property — none of which exist in auth-service's slim schema.prisma. Schema check: the schema defines only auth-domain models (Tenant, User, Session, AuditLog, etc.) plus partner and subscription models; no property-service or pos-service models are present. However, SeedDefaultsService is NOT registered in OnboardingModule.providers (the module comment AP-T01-007 explicitly records the removal) and is NOT imported by any other module. NestJS DI will never instantiate it. The class file was left as dead code when OnboardingService was migrated to PropertyServiceClient in gotcha 11.
  • Reachability: DEAD CODE — not registered in OnboardingModule or any other module provider array. Cannot be reached via any controller or consumer.

NEW FINDINGS — batch-8 (2026-06-08)​

Four new issues surfaced during the in-cluster probe + flow-spec verification round that closed this sweep's P0s.

NF-1 — hmp-housekeeping-service: minibar NOT gateway-routed (two-file-rule gap)​

Severity: P3 (dormant surface — no live callers outside cluster).

The minibar module is NOT registered in hmp-api-gateway/src/config/services.config.ts (no /minibar prefix entry) and has no matching @All('/minibar') + @All('/minibar/*') decorators on ProxyController. All four minibar endpoints verified working via in-cluster direct probe (GET 200 [], consume unknown-item clean 404, replenish 200, charge 200). Zero external clients can reach them today. Classify as gateway gap — two-file-rule violation. Add /minibar → hmp-housekeeping-service:3006 to the gateway config before exposing to dashboard or mobile.

NF-2 — hmp-property-service: /admin/properties/:id/transfer NOT gateway-routed (two-file-rule gap)​

Severity: P3 (operator-only surface — not customer-facing).

/admin/properties/:id/transfer is NOT reachable through the gateway. Gateway services.config.ts has /admin/providers (payment-service) but no /admin/properties prefix, and ProxyController has no matching @All decorators for it. In-cluster probes verified the handler is live (empty body → 400, nonexistent property → handler-404). The operator transfer flow would silently 404 if invoked from an admin dashboard. Add /admin/properties prefix to gateway config before wiring the admin UI.

NF-3 — hmp-property-service: recommendations/generate handler binds @Body to a raw TS type — ZERO validation​

Severity: P2. STATUS: FIXED (2026-06-08) — src/rates/dto/pricing-recommendation.dto.ts + controller + tenant assert + 28 new tests; suite 1049 green. LIVE (deploys 32392+32402): junk payload → 400 VALIDATION_ERROR; cross-tenant propertyId → 403.

POST /rates/recommendations/generate handler in hmp-property-service binds @Body() to a raw TypeScript inline-object type (not a class-validator DTO). This is the same anti-pattern documented in the feedback file: TS inline-object types erase class metadata → ValidationPipe/CustomValidationPipe never runs → malformed payloads pass through silently. A request with missing required fields (e.g. empty body {}) reaches the BAR engine handler without validation errors and can create rows with null/undefined values. Additionally the handler had no assertPropertyTenant call — any authenticated user could generate recommendations for any propertyId regardless of their tenant scope.

Fix (2026-06-08): Created src/rates/dto/pricing-recommendation.dto.ts with four class-validator DTO classes (GenerateRecommendationsDto, AcceptRecommendationDto, RejectRecommendationDto, BatchAcceptRecommendationsDto) covering all four raw-type @Body() bindings in the controller. Bound GenerateRecommendationsDto to generateRecommendations, added @CurrentUser() + assertPropertyTenant(prisma, dto.propertyId, effectiveCallerTenantId(user)) before the BAR engine call. PrismaService injected via the global DatabaseModule. Controller spec (pricing-recommendation.controller.spec.ts) with 28 tests: DTO validation rejects (junk UUID, bad date, negative rate, empty body, non-UUID array element) + tenant-assert behaviours (property-not-found 404, cross-tenant 403, SUPER_ADMIN bypass) + happy-path delegation stubs.

NF-4 — hmp-frontdesk-service: einvoice-report period-bucketing uses UTC midnight for 'today' (IST/UTC window mismatch)​

Severity: P3.

GET /e-invoice/report?period=today uses UTC midnight boundaries for the 'today' window. An IRN generated at 18:44 UTC on 2026-06-08 (= 00:14 IST on 2026-06-09) is bucketed into the next UTC day and therefore NOT returned by period=today when queried on the same UTC date. The row is returned correctly by a custom from/to window that spans the relevant IST hours. No data loss — the row exists and is retrievable. Impact: the daily IRN register viewed by finance staff at EOD IST may appear to miss rows generated after ~18:30 UTC (= ~00:00 IST next day). Fix: compute period boundaries in IST (Asia/Kolkata timezone), consistent with all other date-range reports on the platform.

NF-5 — hmp-frontdesk-service: #7607 corporate room-charge fail-closed (fixed same-day, 2026-06-08)​

Severity: P0 (found-and-fixed in batch-8). STATUS: CLOSED-VERIFIED 2026-06-08 — pos-room-charge-credit-limit.flow.spec.ts 4/4 GREEN post-deploy (build 32387): S1 #7591 real credit columns, S2 #6489 credit-check 200 (COMPANY-folio path passes), S3 over-ceiling 422 POS_ROOM_CHARGE_CREDIT_LIMIT_EXCEEDED, S4 within-ceiling passes. Boards #7591+#6489+#7607 Closed-Verified. US-005B-07 = ✅ GOLD #37.

pos-room-charge-credit-limit.flow.spec.ts (new flow spec added for US-005B-07) live-caught a P0 bug: the credit-limit guard in pos-room-charge.service.ts filtered folios by folioType = 'GUEST' only. Corporate check-in creates a folio with folioType = 'COMPANY'. Every corporate room-charge call hit the credit-limit guard, found zero matching folios (the GUEST filter returned nothing for COMPANY folios), computed balance=0, and rejected the charge with HTTP 403. The entire corporate billing flow was fail-closed. Fixed: frontdesk commit b4e4950 — guard now matches folioType IN ('GUEST', 'COMPANY').

Infrastructure fix required (surfaced by same flow run): FolioServiceClient in hmp-cross-service-urls was resolving to localhost:3005 in K8s (missing env-var entry). Added FOLIO_SERVICE_URL pointing to the in-cluster hmp-frontdesk-service DNS name — infra commit dc9f88ba. Without this, every cross-service folio credit-check resolved to localhost inside a different pod and timed out.

NF-6 — hmp-infrastructure: ArgoCD hmp-_shared Application not generated on test (P1, 2026-06-08)​

Severity: P1. Status: Open — hand-patched live as stopgap; filed as Boards #7608.

The _shared ArgoCD Application (which governs the hmp-apps/_shared Kustomize layer containing shared ConfigMaps including hmp-cross-service-urls) is NOT generated on the test cluster. Root cause: the ArgoCD Application name is derived from the directory name _shared, producing hmp-_shared — a name containing an underscore, which is invalid in Kubernetes resource names (RFC 1123: only lowercase alphanumeric and hyphens). ArgoCD silently skips Application generation for invalid names.

Impact: The hmp-cross-service-urls ConfigMap (which carries all *_SERVICE_URL env vars consumed by every backend Deployment) is GitOps-orphaned on test — changes committed to the base manifest are NOT automatically synced. Any change to cross-service URL config (e.g. the FOLIO_SERVICE_URL fix in infra dc9f88ba) requires a manual kubectl apply to take effect on test.

Stopgap applied: kubectl apply -f kubernetes/base/apps/_shared/configmap-cross-service-urls.yaml --context keynova-test (hand-patched live 2026-06-08 to unblock US-005B-07 GOLD verification).

Permanent fix required: Rename the directory from _shared to shared (or hmp-shared) and update all Kustomize overlay references + the ArgoCD ApplicationSet generator pattern. Must coordinate with ArgoCD ApplicationSet generator regex to ensure the renamed directory is picked up. Track as #7608.

NF-7 — hmp-banquet-service: GroupRoomingDto missing confirmationNumber / arrivalDate / departureDate (P3, 2026-06-08)​

Severity: P3 (cosmetic contract gap — no crash, no data loss).

GroupRoomingDto in hmp-banquet-service (src/events/dto/group-rooming.dto.ts) does not declare confirmationNumber, arrivalDate, or departureDate fields. The dashboard event-group rooming view (hmp-web-dashboard/src/app/(dashboard)/banquet/events/[id]/group/page.tsx) binds the response as any and renders these three fields inline — they resolve to undefined on every row, so the confirmation number and date columns are always blank in the UI even when the underlying reservation data is available. Found during the @hmp/service-clients@1.28.3 rewire (27b50e3). Fix: add the three fields to GroupRoomingDto (with @IsOptional() + appropriate decorators) and populate them from the reservation-service lookup already performed in GroupsService.getGroupRooming.