Read-Model Include Sweep — #7598 (2026-06-07)
SWEEP CLOSURE (2026-06-08)
All items resolved. Deployed to test and probed 2026-06-08.
| Tier | Count | Final state |
|---|---|---|
| P0 | 13 | All 13 Closed-Verified (individual Boards items #7598 links) |
| P1 | 37 | All 37 FIXED + deployed (9 clusters, commits listed per item) |
| P2 | 17 | 13 FIXED · 2 DESIGNED (P2-1 company publisher + P2-14 engagement events) · 2 documented data-path gaps (FeedbackSurvey hydration follow-up; CRM RoomReservation sync deferred) |
| info | 2 | 1 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 frontdeskb4e4950→ Closed-Verified - #7608 (P1) — ArgoCD
hmp-_sharedApplication not generated on test (invalid Kubernetes namehmp-_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/UpdateCompanyDtowhitelist 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/transferNOT gateway-routed — P3, dormant, documented - NF-3:
recommendations/generateunvalidated@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
_sharedApplication failure — P1, hand-patched, #7608 filed - NF-7:
GroupRoomingDtomissingconfirmationNumber/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 hasmodel TenantReadModel { tenantId String @id ... @@map("tenant_read_model") }— Prisma generates accessorprisma.tenantReadModel, NOTprisma.tenant.prisma.tenantisundefinedat runtime →undefined.findUnique(...)throws immediately. Additionally, even if the accessor were corrected: (a) the PK istenantIdnotid, sowhere: { id: tenantId }would fail; (b)settingsis not a column onTenantReadModel(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 carrysettings), 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 localCompanyread-model schema (prisma/schema.prisma lines 918-932) has exactly: id, propertyId, name, contactPerson, email, gstin, isActive, createdAt, updatedAt, folios[]. NOcurrentBalancecolumn. Prisma rejects anupdatewith an unknown field at query-validation time (the error fires even if zero Company rows exist) → the entire$transactionrolls 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 } } })insettleCompanyBalance. Same root cause as line 114-119:Company.currentBalancedoes 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.folioTransactionis not a model in prisma/schema.prisma (belongs to hmp-frontdesk-service). At runtimeprisma.folioTransactionisundefined→ TypeError inside the transaction. Companion issue: the payment.create at line 169 also suppliesreceivedBy: '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.folioTransactionnot in local schema. AlsoreceivedBy: '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.unitPricescalar (migration20260607150000_add_minibar_item_unit_price, idempotent),itemNamealready existed; dropped the phantomitem @relation+ everyinclude: { item: true }; removed@ts-nocheck; code now reads row scalars. Charge path hardened:resolveUnitPricethrows 422UnprocessableEntityExceptionwhen price is missing/≤0 — never posts a ₹0/null line. Tests use non-permissive mocks (itemundefined). 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/updateMinibardrop theiteminclude and readitemName/unitPrice;updateMinibarthrows 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 atx.company.updateMany({ data: { contactPerson, email, phone } })inside a$transaction(async (tx: any) => ...). The guest-service Company model (schema.prisma lines 103-125) has columnsname,code,email,phone,gstin,address,city,state,country,postalCode,creditLimit,isActive— NOcontactPersoncolumn exists.txis typedanyso tsc is silent. Prisma throws "Unknown argumentcontactPerson" at runtime, aborting the entire $transaction. The guest's PII anonymisation, theguestIdentityerasure, theconsentLogwrite, and thegdprErasureLogrow 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).pricingRecommendationand upserts with all three missing fields plus the invalid enum value. Prisma will throw 'Unknown field X' at runtime on first upsert attempt. ThewherekeypropertyId_roomTypeId_date_sourceis 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
$transactionand at line 89 callstx.user.updateMany({...})then at line 95 callstx.auditLog.create({...}). Both will throw Prisma P2009 'Cannot find model' on first execution. The transaction is entered asCrossServicePrisma(type alias forany) 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 Stringis 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)andNumber(company.creditLimit)read from the barexPrisma.company.findUniqueresult. Schema confirms these columns do not exist onCompany. At runtimecompany.currentBalanceandcompany.creditLimitare bothundefined.Number(undefined)=NaN. The credit-limit guardNumber(company.creditLimit) > 0 && newBalance > Number(company.creditLimit)evaluates toNaN > 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: NaNin 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(...).ratePlanis not a model in prisma/schema.prisma (the NOTE at schema line 509 explicitly states RatePlan was deleted). At runtimethis.prisma.ratePlanisundefined, so.findFirstthrows 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.UuidandratePlanId String @db.Uuid— no roomType/@relation and no ratePlan/@relation.findOne()at line 227 uses xPrisma.group.findUnique withinclude: { 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.UuidandroomId String? @db.Uuid— no roomType/@relation and no room/@relation. Lines 247-252 in findOne() includeroomReservations: { 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 } }.roomis not a relation on the local RoomReservation model (onlyroomId 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(...).maintenanceRequestis not a model in prisma/schema.prisma (belongs to hmp-asset-service). At runtimethis.prisma.maintenanceRequestisundefined, so.createthrows 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(...).guestIdentityis not a model in prisma/schema.prisma (belongs to hmp-guest-service). At runtimethis.prisma.guestIdentityisundefined→ 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(...).guestPreferenceis not a model in prisma/schema.prisma (belongs to hmp-guest-service). At runtimethis.prisma.guestPreferenceisundefined→ 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(...).outletis not a model in prisma/schema.prisma (belongs to hmp-pos-service). At runtimethis.prisma.outletisundefined→ 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(...).menuCategoryis 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(...).roomis not a model in prisma/schema.prisma (the NOTE at schema line 509 explicitly states Room was deleted). At runtimethis.prisma.roomisundefined→ 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.roomTypeis not a relation on the local RoomReservation model (schema lines 210-237, onlyroomTypeId 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 offolioType,companyId,currencyexist 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.ratePlanis not a model in prisma/schema.prisma. At runtimeprisma.ratePlanisundefined→ 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.roomTypeis 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 noreservationsfield at all. Prisma will throwPrismaClientValidationError: Unknown field 'reservations' for include statement on model 'Group'at runtime on every call that reaches this branch (i.e. wheneverevent.groupIdis non-null). Schema check: grep for 'reservations' in schema.prisma returns zero hits on the Group model — onlyeventBookings 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
reservationsrelation 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. Theselect: { ..., 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 aPromise.all. Any of the four will throw at runtime. Thereservationquery (line 60) IS in the local schema (Reservation model), butoutlet,menuCategory,pOSTable, andhousekeepingTaskare 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, andthis.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 byreconcileHousekeepingorreconcilePosOrderwill throw. TheauditLog.createon line 106 is insideprocessMutationand 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).tenantConfigfor 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).customFieldDefinitionfor 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).channelConnectionfor 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 nestedfolio.reservation.propertyIdfilter — 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 localMenuCategoryschema (idempotent migration20260607000000_menu_category_property_id). Fixed filter tocategory: { propertyId }scalar. AddedMenuSyncConsumer(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.tssubscribes topos.menu.changed, fetchesGET /api/outlets/:id/menufrom pos-service (INTERNAL_SERVICE_TOKEN), upsertsMenuCategory(withpropertyId) +MenuItemrows idempotently. Best-effort. 6 specs. Follow-up #7599: addPosServiceClient.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/billingContactEmailfrom SELECT (not in local Tenant schema). Now usessupportEmail(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.findFirstwithinclude: { 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.mddefinescompany.created/company.updated/company.deletedtopics and the ownership model. Security fix: #7610 (company CRUD mass-assignment) Closed-Verified (2026-06-08) —CreateCompanyDto/UpdateCompanyDtowhitelist added, blocking unintended field injection on company create/update.
hmp-pos-service — menu/menu.service.ts : 565
- Recipe.yieldQuantity — suspicious-cast
- Evidence: Schema check:
Recipemodel (prisma/schema.prisma:515) declares the field asyield Decimal, notyieldQuantity. The code is cast via(this.prisma as any).recipe.findUnique(...)which bypasses tsc. At line 565 the response is built asyieldQuantity: Number(recipe.yieldQuantity ?? 1)—recipe.yieldQuantityresolves toundefinedat runtime because the column does not exist, so the?? 1fallback silently returns1on 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 aselect/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; readrecipe.yielddirectly, 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.erasedevent 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. Onlysrc/events/consumers/guest-erased.consumer.tsremains. 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.ratePlanandreservations.folios.transactionsvia typedthis.prisma.guest.findFirst. The schema definesRoomReservation.ratePlan RatePlan?(schema.prisma line 557) andFolio.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 upsertsPropertyandReservationrows — NO writer exists forrate_plans,room_reservations,folios, orfolio_transactionstables 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 yieldsratePlan: nullandtransactions: []at runtime. The AI summary'spreferredMealPlancomputation (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. PhantomratePlan+transactionsincludes removed; real data fetched at query-time via ServiceClients. LIVE:GET /guests/:id/ai-summary200 (was phantom-include crash). Follow-up:FeedbackSurveyhydration —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.roomTypeandfeedbackResponses.surveyvia typedthis.prisma.guest.findFirst. Schema relations exist:RoomReservation.roomType RoomType(schema.prisma line 556) andFeedbackResponse.survey FeedbackSurvey(line 652). Butroom_types,room_reservations,feedback_surveys, andfeedback_responsestables 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. PhantomroomType+feedbackResponses.surveyincludes removed; ServiceClient reads injected. LIVE:GET /guests/:id/360200 (was phantom-include crash). Follow-up:FeedbackSurveyhydration still returnssurvey: 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.roomTypevia xPrisma (typed any). Schema relation RoomReservation.roomType exists (schema.prisma line 556) so no crash. Theroom_reservationsandroom_typestables are never written to by any consumer in this service; everyroomReservationsarray in the history response is always empty. The US-004-001 fix comment at line 944 already removed theroomrelation crash but theroomTypeinclude 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. PhantomroomTypeinclude removed; ServiceClient reads injected. LIVE:GET /guests/:id/history200 (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.GdprModuleimportsServiceClientsModule.register();GdprServiceinjects@Optional() folioClient?. Deadfolios: truePrisma include removed. Each reservation getsfolios: FolioDto[]on success orfinancialDataUnavailable: trueon 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 typedthis.prisma.guest.findUnique. Schema relationReservation.folios Folio[]exists (schema.prisma line 539). Thefoliostable 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 returnsreservations[].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+PropertyPhotomodels added with idempotent migration20260708200000;content.service.tsrewritten to typed Prisma (noanycast); 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 readsproperty.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 —
brandNameresolved fromtenant.name(best local source, avoids phantom column);logoUrlandprimaryColorreturned as explicitnull(notundefined). Auth-serviceTENANT_*events carry no branding fields — FU-86 follow-up: addbrandName/logoUrl/primaryColorto 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.getRoomTypesis now the authoritative source (no publisher exists for room-type events —room-types.service.tshas zero event publishes;EventTopicsenum has noROOM_TYPE_*entries). The fictitious₹5000fallback 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;
getRoomServiceMenuviaPropertyServiceClientreplaces phantomroom_typeslookup; 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.searchReservationsreplace phantomprisma.room.count+prisma.roomReservation.count. HonestINSUFFICIENT_DATAstate 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.searchReservationsreplaces phantomprisma.roomReservation.count; raw SQL LEFT JOINs against emptyroom_reservationsremoved. 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.tsreads now usetenantReadModel(PKtenantId; populated byTenantReadModelConsumer), nottenant(always-emptytenantstable). Spec uses non-permissive mock (notenantaccessor). EP-014 failure VERIFIED pre-existing at parent commit852de67(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:
4fd6bc9addedMenuSyncConsumer(pos.menu.changed→ MenuCategory/MenuItem upserts in notification-service DB).852de67rewiredgetMenuCatalogontoPosServiceClient.getRoomServiceMenu(@hmp/service-clients@1.28.3) as the live-query path. Rows populate onpos.menu.changedevents 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 witha.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
addedAtfromAmcContractResponseDto.assets(nowArray<{ assetId: string }>) and fromtoResponsemapper. 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 = anyand accessestx.roomType,tx.ratePlan,tx.cancellationPolicy,tx.outlet, andtx.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,SeedDefaultsServiceis NOT registered inOnboardingModule.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 whenOnboardingServicewas migrated toPropertyServiceClientin 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.