Property #1 Gold-Standard Fix Plan — 2026-04-28
Companion to: property-1-readiness-audit-2026-04-28.md (verdict: NO-GO; 47 P0 + 41 P1 + 39 P2).
Goal of this plan: flip the verdict to GO by closing every P0 and P1 with a gold-standard fix — root-cause, not band-aid; one canonical implementation, not a parallel one; type-safe schema, not JSON blob; observable, not silent; tested, not hoped.
Audience: every engineer or agent assigned an AP-* item. Read this in full before opening a PR.
Part 1 — Gold-Standard Charter (mandatory, applies to every fix)
These twelve principles are the deal. A PR that violates any of them is rejected, regardless of severity.
1. Fix the root cause, delete the workaround
A "real" implementation alongside an old stub is not a fix — it is a parallel implementation, which is worse than either. When you ship the canonical path, delete the legacy code in the same PR. If deletion is not safe yet, file a follow-up AP-CLEANUP-* item and reference it.
Examples already in the codebase (each is a debt to clear, not a model to copy):
- Two C-Form controllers (
frontdesk/cform.controller.ts+compliance/cform.controller.ts) - Two hardware stacks (
@hmp/hardwarelegacy +hmp-payment-service/src/adapters/pinelabs/) - Two room-move services (
RoomOpsService.roomMove+CheckInService.roomMove) OnboardingServiceself-taggedDEPRECATED — uses xPrisma anti-patternbut still wired
2. No silent fallbacks. In production, missing config is a boot failure.
@Optional() injection of security-critical services is a footgun. EINVOICE_MOCK=true defaulted on prod compose is a footgun. bookingEngineApiKey: 'hmp-default-...' shared seed is a footgun.
Rule: every required prod config (encryption key, NIC creds, Razorpay creds, INTERNAL_SERVICE_TOKEN, JWT public key) must have a startup validator that throws when NODE_ENV === 'production' and the config is missing or matches a known development/test value. Add the validator inside the module's onModuleInit and unit-test it.
3. Schema-first: typed columns, not JSON blobs.
Where the audit found values stuffed into a JSON settings field (e.g. Property.settings.businessDate, Property.settings.tapeChartColors, Property.settings.bookingEngineApiKey), promote them to typed columns with constraints, indexes, and migration history. Use JSON only for genuinely free-form per-tenant overrides where querying is never required.
Apply specifically to: currentBusinessDate Date? on Property; irn/signedQRCode/ackDate on Folio; idempotencyKey on Reservation; latitude/longitude/googleMapsUrl on Property; frroCity on Property; inclusive Boolean on TaxConfiguration; visa columns on Guest.
4. One implementation, not two.
For every audit-flagged duplicate path, designate the canonical owner and migrate consumers to it before deleting the rival. Concretely:
- C-Form:
frontdesk/cform.*is canonical. Deletecompliance/cform.*(and verify dashboard does not call it). - Hardware:
hmp-payment-service/src/adapters/pinelabs/is canonical. Strip@hmp/hardwarefromhmp-frontdesk-service/package.jsonand any other dep lists. - Room-move: consolidate into one service. The CheckInService variant has the pricing safeguard; that is the keeper.
- Onboarding: rebuild
OnboardingServicearoundPropertyServiceClient(no xPrisma).
5. Idempotency at every boundary.
Every external write (vendor API call, internal POST that creates an entity, every webhook delivery, every Kafka publish-consume pair) carries an idempotency key, and the receiver enforces dedup using either a unique constraint or a processed_events table.
Concrete obligations:
ReservationgetsidempotencyKey String? @unique(or@@unique([propertyId, idempotencyKey])).- Razorpay Orders create body sets
payment_capture: 1and the request carries an idempotency key in the header (already partially done atrazorpay-gateway.adapter.ts:87,131,217— extend to all paths). - Every Kafka consumer must dedup on a domain key; the
EventStoretable is the source of truth.
6. Observability is part of the feature, not optional polish.
Every new integration logs a structured event (correlation ID + tenantId + propertyId + operation) at INFO on success and WARN/ERROR on failure. Every Kafka publish-consume pair appears as an Outbox row. Every external vendor call is wrapped in @hmp/observability HTTP histogram.
Concrete: a Pino log line at every state-changing call; a Prometheus histogram for every external HTTP call; an alert rule for any failed-job DLQ depth > 0.
7. Tests before the fix (TDD discipline).
For every bug-class fix (rate recalc, rollback filter, B2B IRN), write the failing test first. Submit the test in a separate commit before the fix commit so reviewers can see the bug reproduced. No "fix + test in one commit" shortcuts.
For every new feature, the order is: contract test → unit test → integration test → flow test → e2e test. Skipping any layer requires a written justification in the PR description.
8. Cross-service correctness via ServiceClient + receiver-side assertPropertyTenant.
The xPrisma cast pattern is dead. Every cross-service read uses the typed client in @hmp/service-clients. Every receiving controller's first line of defence is await this.<x>Client.assertPropertyTenant(propertyId, tenantId). Gateway 5-min cache is not a substitute.
Reference: hmp-docs/docs/architecture/cross-service-communication.md.
9. Compose env hygiene; no _MOCK=true outside dev-all.yml.
QA, staging, prod compose files MUST NOT carry mock flags. Required prod creds are placeholders fed from vault/SSM/ExternalSecrets, never inline in YAML.
CI gate (AP-CC-1 builds the gate): grep -E '(EINVOICE_MOCK|HARDWARE_MOCK|EMAIL_MOCK)\s*:\s*"true"' docker-compose.{qa-aws,staging-aws,prod-aws}.yml must return zero rows.
10. Decide and commit on every "build OR descope" question.
Mobile frontdesk, kiosk self-check-in, mobile reservations, mobile manager NA widget — each has a "build OR descope" gate in this plan. The decision must be documented in hmp-docs/docs/platform-registry/phase1-mobile-scope-decision-2026-04-28.md before the dependent action items start. A "deferred to Phase 2" decision is fine; a silent omission is not.
11. Update CLAUDE.md and the platform-registry in the same PR as the fix.
Every audited service's root CLAUDE.md is the working source of truth. Stale CLAUDE.md is a defect. After every fix:
- Add or update the relevant module/endpoint/schema row in the service's root
CLAUDE.md. - If a per-folder
CLAUDE.mdexists in the affected directory, update it too. - Add the corresponding registry entry under
hmp-docs/docs/platform-registry/_data/(use cases, drift, gates, compliance — whichever applies). - If the fix establishes a new architectural pattern, add an entry in
hmp-docs/docs/architecture/.
A PR that ships code without the CLAUDE.md / registry update is incomplete and will be rejected.
12. Migration safety.
Every Prisma schema change goes through migrate dev → migrate deploy, with a rollback SQL committed alongside. No prisma db push --force-reset against shared databases. Backfill scripts run separately under feature-flag, idempotent, and resumable.
For every new column on a non-empty table:
- Add column nullable + default in migration A.
- Backfill in migration B (separate deploy).
- Flip to NOT NULL in migration C (separate deploy).
Migration files live in the per-service
prisma/migrations/directory; backfill scripts live inprisma/migrations-data/.
Part 2 — Mandatory Deliverables Per Fix
Every AP-* item that ships must include all of the following. PRs missing any one of these are not "Definition of Done."
| # | Deliverable | Owner | Where it lives |
|---|---|---|---|
| A | Code change implementing the gold-standard fix (root cause, not workaround) | Eng | service repo |
| B | Failing-then-passing tests (one or more layers per principle 7) | Eng | service repo test/ or e2e/ |
| C | Root CLAUDE.md of every affected repo updated (endpoints, modules, gotchas, schema row) | Eng | each affected repo |
| D | Per-folder CLAUDE.md updated where one exists | Eng | each affected repo |
| E | Registry entry under hmp-docs/docs/platform-registry/_data/ | Eng | hmp-docs |
| F | If schema change: migration file + backfill SQL + rollback SQL | Eng | service repo prisma/migrations/ |
| G | If new external vendor call or compliance-impacting: runbook in hmp-docs/docs/operations/ or compliance/ | Eng | hmp-docs |
| H | If feature flag introduced: documented in service root CLAUDE.md + flag default | Eng | service repo |
| I | PR description referencing this plan, AP-ID, gold-standard principles satisfied, what was deleted | Eng | PR body |
| J | If a new event topic, consumer, or Kafka producer is introduced: entry in hmp-shared-packages/packages/messaging/src/events/event-types.ts | Eng | hmp-shared-packages |
| K | One CI run green on the feature branch before opening PR | Eng | CI |
Branch + commit conventions
- Branch:
audit/AP-TXX-NNN-<short-slug>(e.g.audit/AP-T08-001-pdf-irn-real-lookup). - Commit format: subject line
audit(AP-TXX-NNN): <imperative summary>; body explains why and lists what was deleted. - One AP-ID per PR. Do not bundle.
- Co-Authored-By trailer is required when an agent contributed.
Definition of Done (DoD) for any AP-*
- All twelve gold-standard principles satisfied for this fix.
- All eleven deliverables above committed.
- Failing test exists in commit history; passing test in latest commit.
- CI green.
- CLAUDE.md + registry updated in same PR.
- PR reviewed by at least one engineer who did not author it.
- The corresponding row in
phase1-action-plan.md(or this document's tracker) flipped to "DONE" with the merge SHA.
Part 3 — Fifteen Architectural Tracks
Every action item belongs to exactly one track. Track-level architectural decisions are stated once; per-item rows then reference them.
Track 1 — Compliance & Legal
Vision: every Indian-statutory artifact (GST e-invoice, Form C / FRRO, DPDPA erasure, GSTR aggregates) must be backed by real vendor integration, not mocks; and by a written compliance runbook with a CI guard against regression.
Architectural decisions:
EINVOICE_MOCKbecomes development-only. Prod boot fails whenNODE_ENV=production && EINVOICE_MOCK=true.EinvoiceJob.irn/ackNumber/ackDate/signedQRCodeare denormalised ontoFolioafter success; PDF reads fromFolio, not from a JOIN.- B2B path (corporate stays) is first-class.
EInvoiceServicealways callsGuestServiceClient.getCompany+getGuestAddresses; nullable result yields B2C/URP, not silently-null company. GUEST_ERASEDevent is consumed by every service that holds guest PII (frontdesk, pos, housekeeping, analytics, crm, payment, notification, banquet, spa). Each consumer scrubs its own tables with idempotency.- Foreign-guest visa is a first-class
GuestVisamodel (FK → Guest), not a polymorphicGuestIdentity{idType:OTHER}. - Production refuses to boot if
ENCRYPTION_KEYis unset. - A 90-day retention timer is scheduled at checkout commit; cron writes
GdprErasureLogwithlegalBasis='RETENTION_EXPIRY'.
P0 items:
| AP-ID | Gold-standard fix | Acceptance criteria | CLAUDE.md target | Registry target |
|---|---|---|---|---|
| AP-T08-001 | Replace irn = 'Pending' literal with a real lookup. PDF service reads from the new Folio.irn/Folio.signedQRCode columns (added by AP-T08-006). When folio has no IRN yet, the PDF clearly states "DRAFT — IRN PENDING" with a watermark, not a literal "Pending" embedded in the QR placeholder. | A folio with a generated EInvoiceJob produces a PDF whose IRN string equals the job's IRN; QR code base64 round-trips through a NIC-format validator. | hmp-frontdesk-service/CLAUDE.md (folio + invoice tables) | _data/use-cases/uc-frontdesk-einvoice-irn.yaml |
| AP-T08-002 | Add NIC_CLIENT_ID/SECRET/USERNAME/PASSWORD/GSTIN/EINVOICE_ENV env placeholders. Prod-startup validator rejects boot when `NODE_ENV=production && (EINVOICE_MOCK!=='false' | any NIC var missing)`. Add CI grep gate. | Boot of hmp-frontdesk-service with NODE_ENV=production and EINVOICE_MOCK=true exits non-zero with a clear error. | |
| AP-T08-003 | Wire GuestServiceClient.getCompany + getGuestAddresses into EInvoiceService.generateIrnForFolio. null result legitimately produces B2C; non-null result populates the B2B section of the IRN payload with PoS calculation. | A reservation tied to a Company produces an IRN payload with Buyer.GSTIN populated; a walk-in produces Buyer.URP. Verified by unit test + flow test. | hmp-frontdesk-service/CLAUDE.md einvoice + cross-service section | _data/use-cases/uc-frontdesk-einvoice-b2b.yaml |
| AP-T08-004 | New GuestRetentionTimer model in guest-service ({guestId, expiresAt, basis}). Checkout publishes GUEST_RETENTION_SCHEDULED. Daily cron in guest-service scans timers ≤ now, calls gdpr.service.eraseGuest(guestId), writes GdprErasureLog row, publishes GUEST_ERASED. | A reservation checked out today produces a timer row with expiresAt = today+90d; flipping the clock 91 days produces a GdprErasureLog row. | hmp-guest-service/CLAUDE.md gdpr module, hmp-frontdesk-service/CLAUDE.md checkout | _data/compliance/dpdpa-retention.yaml |
| AP-T08-005 | New GuestErasedConsumer per service (frontdesk, pos, housekeeping, analytics, crm, payment, notification, banquet, spa). Each consumer scrubs its denormalised PII (Folio.guestName, Order.guestName, Task.assignedToName, mv_*, CommunicationLog.metadata, Payment.metadata, etc.) idempotently and emits GUEST_ERASED_ACK. | Erasing a guest produces ACK events from every service holding their PII; manual SQL audit confirms no plaintext name/passport/visa remains. | each service's root CLAUDE.md (consumers section) | _data/compliance/guest-erased-consumers.yaml |
| AP-T06-001 | IndianFrroClient interface in @hmp/integrations defining submitFormC, getStatus, markSubmitted. Initial impl is ManualUploadFrroClient (today's behaviour: returns submission URL, accepts manual ack). Future impl FrroPortalClient slots in via env. Runbook in hmp-docs/docs/operations/frro-form-c.md walks staff through the manual flow. | A C-Form generated for a non-Indian guest emits a FORM_C_GENERATED event with the manual upload URL; a "mark submitted" call records the ack number against the row. | hmp-frontdesk-service/CLAUDE.md cform module | _data/compliance/form-c-frro.yaml |
| AP-T06-002 | Drop the idType=OTHER visa hack. New GuestVisa model in guest-service ({id, guestId, visaNumber, visaType, issuedDate, expiryDate, issuedAt, frroCity}). DTO splits cleanly; CForm generation reads from GuestVisa, not GuestIdentity. Migration converts existing rows. | Foreigners with passport+visa produce two distinct rows (passport identity + visa); the C-Form PDF carries both correctly. | hmp-guest-service/CLAUDE.md Guest table + new GuestVisa | _data/use-cases/uc-guest-visa-record.yaml |
| AP-T06-009 | Boot validator on every service that uses @hmp/security field encryption: throws if NODE_ENV=production && !ENCRYPTION_KEY. Add to all 13 backend services in their app.module.ts onModuleInit. | Prod-mode boot without ENCRYPTION_KEY exits with code 1 within 200ms. | each service's root CLAUDE.md (security section) | _data/gates/prod-encryption-key-required.yaml |
P1 items in this track: AP-T08-006 (Folio IRN columns — depends on the migration discipline of principle 12), AP-T08-007 (credit-note IRN through queue), AP-T08-009 (NIC webhook URL registration).
Track 2 — Payment Platform Hardening
Vision: every tenant has a per-tenant MerchantAccount with encrypted credentials provisioned through the admin onboarding wizard; Razorpay calls always pass an idempotency key and explicit payment_capture: 1; missed webhooks self-heal via a reconciliation job; hardware terminals run through one canonical adapter.
Architectural decisions:
- The
MerchantAccounttable (with encrypted creds at rest) is the only source of payment credentials. Compose-injectedRAZORPAY_*env vars are forbidden in QA/staging/prod (only used by local dev seed when scaffolding the firstMerchantAccountrow). - Admin onboarding wizard step "Payment Provider" calls
POST /api/merchant-accountswith sandbox creds; tenant later edits to live via the same endpoint. - A new
WebhookReconcileServiceruns every 15 minutes per tenant: fetches Razorpay/payments?from=lastCursorand reconciles againstPaymentrows in IN_PROGRESS state. - Pinelabs adapter under
hmp-payment-serviceis canonical;@hmp/hardwareis removed fromhmp-frontdesk-service/package.json. - All four compose files (
dev-all,qa-aws,staging-aws,prod-aws) gain a CI-checked invariant:HARDWARE_MOCK=trueonly indev-all.
P0 items:
| AP-ID | Gold-standard fix | Acceptance criteria | CLAUDE.md target | Registry target |
|---|---|---|---|---|
| AP-T12-001 | Add a Razorpay sandbox MerchantAccount seed row for tenant 1 in seeds/seed-keynova-payment.sql, encrypted via PAYMENT_KMS_KEY_VERSION=v1. Document the seed-key rotation runbook. | GET /api/merchant-accounts on QA returns one row with mode=SANDBOX after seed-master.sh. | hmp-payment-service/CLAUDE.md MerchantAccount section, hmp-infrastructure/CLAUDE.md seeds | _data/use-cases/uc-payment-merchant-account.yaml |
| AP-T12-002 | Build admin-portal "Payment Provider" wizard step. Calls POST /api/merchant-accounts (auth as SUPER_ADMIN with x-impersonated-tenant-id when admin acts on tenant). Stores creds encrypted; never logs them. After save, performs validateCredentials and surfaces failure. | Tenant onboarding completes only after validateCredentials succeeds for at least one MerchantAccount; verified by Playwright e2e. | hmp-admin-portal/CLAUDE.md onboarding wizard, hmp-payment-service/CLAUDE.md merchant-account flow | _data/use-cases/uc-tenant-payment-onboarding.yaml |
| AP-CC-3 | RS256 migration for /housekeeping, /kitchen, /events WebSocket gateways. Each gateway loads JWT_PUBLIC_KEY; HS256 path remains for dev, fail-closed for prod. Mobile/kiosk clients auto-handle algorithm negotiation. | Setting NODE_ENV=production + missing JWT_PUBLIC_KEY → boot fails. HS256-signed token to /kitchen in prod is rejected. | hmp-housekeeping-service/CLAUDE.md, hmp-pos-service/CLAUDE.md, hmp-api-gateway/CLAUDE.md | _data/gates/ws-rs256-prod.yaml |
| AP-CC-4 | Strip EINVOICE_MOCK=true and HARDWARE_MOCK=true from staging-aws.yml + prod-aws.yml. Remove inline credentials. Use ${RAZORPAY_KEY_ID} placeholders sourced from external secrets. CI grep gate enforces. | grep -E '(EINVOICE_MOCK|HARDWARE_MOCK)\s*:\s*"true"' docker-compose.{prod-aws,staging-aws}.yml returns 0 rows. | hmp-infrastructure/CLAUDE.md deploy section | _data/gates/no-prod-mocks.yaml |
| AP-CC-5 | Provision SendGrid creds across QA + prod via External Secrets. Templates already exist in notification-service. Add e2e test covering invoice email delivery via SendGrid sandbox. | Sending an invoice on QA produces a SendGrid message ID logged at INFO level. | hmp-notification-service/CLAUDE.md channels section | _data/use-cases/uc-notification-email-invoice.yaml |
P1 items: AP-T12-003 (payment_capture: 1), AP-T12-004 (WebhookReconcileService), AP-T12-005 (override prod compose HARDWARE_MOCK). All inherit the architectural decisions above.
Track 3 — Data Integrity & Tenant Isolation
Vision: every cross-service write is idempotent; every cross-service read uses ServiceClient with receiver-side assertPropertyTenant; every JSON-blob "settings" config is migrated to a typed column; every API key is per-property and DB-backed.
Architectural decisions:
Reservation.idempotencyKeyisString? @unique; absence allows legacy rows. Booking-engine, dashboard quick-create, and channel inbound all stamp keys.ApiKeyis a first-class table ({id, tenantId, propertyId, keyHash, label, lastUsedAt, expiresAt}).IApiKeyServiceis provided in reservation-service AppModule and looks up by SHA-256 of presented key.Property.currentBusinessDate Date?replacesProperty.settings.businessDate. Property-service exposesPATCH /properties/:id/business-date(internal-only); frontdesk'sadvanceBusinessDatecalls this.Folio.irn/signedQRCode/ackDatedenormalised columns; PDF reads from folio.TaxConfiguration.inclusive Booleancolumn.CancellationPolicy.rulesJSON has a documented JSON-schema validated at API boundary.Guestschema gainsnationalityalready present, plus the newGuestVisamodel from Track 1.CalendarService.getCalendarandcreateQuickReservationcallassertPropertyTenant(propertyId, tenantId)first.- The
xPrismacast pattern is forbidden in new code; existing usages get a deprecation comment with target removal date.
P0 items:
| AP-ID | Gold-standard fix | Acceptance criteria | CLAUDE.md | Registry |
|---|---|---|---|---|
| AP-T03-001 | Reservation.idempotencyKey String? @unique migration; BookingEngineController.createReservation and ReservationsController.create accept Idempotency-Key header; second submit returns the first reservation with HTTP 200 and a Idempotency-Replay: true header. Rejection on key collision with different body. | Two consecutive POSTs with same key → one row, two responses with same id. Different body with same key → 409. | hmp-reservation-service/CLAUDE.md reservation table + endpoints | _data/use-cases/uc-booking-idempotency.yaml |
| AP-T03-002 | New ApiKey table; IApiKeyService impl in reservation-service; seed migration converts existing property.settings.bookingEngineApiKey to per-property hashed ApiKey rows. Rotation endpoint POST /api/properties/:id/api-keys/rotate emits API_KEY_ROTATED. Old shared seed key is revoked. | Public booking-engine endpoints reject the old shared key after migration; per-property key hash is stored, never plaintext. | hmp-reservation-service/CLAUDE.md booking-engine module, hmp-property-service/CLAUDE.md api-key section | _data/use-cases/uc-property-api-key.yaml |
| AP-T13-001 | Add await this.propertyClient.assertPropertyTenant(propertyId, tenantId) to CalendarService.getCalendar AND createQuickReservation. tenantId comes from AsyncLocalStorage, not headers. | Call with valid JWT for tenant A but propertyId belonging to tenant B → 403. | hmp-frontdesk-service/CLAUDE.md calendar module | _data/drift/calendar-tenant-assert.yaml |
| AP-T02-001 | Add latitude Float?, longitude Float?, googleMapsUrl String? to CreatePropertyDto + UpdatePropertyDto. Map in service. Add JSON-schema validation on googleMapsUrl (must match `^https://(www. | maps.)?google.[a-z.]+/maps`). | PUT /properties/:id round-trips all three fields. Invalid URL rejected with 400. | hmp-property-service/CLAUDE.md property table |
| AP-T01-007 | Replace OnboardingService xPrisma anti-pattern with proper PropertyServiceClient + RoomTypeServiceClient calls. Saga commits each step or compensates. Delete the old service in the same PR. | Onboarding a tenant from signup → property → room types → rooms → rate plans → tax → policy completes via the canonical client chain; no xPrisma reference remains in auth-service. | hmp-auth-service/CLAUDE.md onboarding module | _data/use-cases/uc-tenant-onboarding-saga.yaml |
P1 items: AP-T08-006 (Folio IRN columns), AP-T09-004 (Property.currentBusinessDate column), AP-T02-005 (TaxConfiguration.inclusive), AP-T02-006 (CancellationPolicy.rules schema), AP-T06-002 (visa columns — listed in Track 1). All apply the typed-schema discipline.
Track 4 — Cross-Service Eventing
Vision: event topics are a typed contract; every published event has at least one consumer or is removed; the analytics dispatcher subscribes to the full nine-topic spec; every consumer dedups; every Kafka producer-consumer pair appears in event-types.ts.
Architectural decisions:
- The canonical event taxonomy lives in
hmp-shared-packages/packages/messaging/src/events/event-types.ts. Producer-side and consumer-side import from this single source. - Rename
folio.charge.posted→folio.postedto match spec, OR add an alias inevent-types.tsand bump major. Decision: rename, single source of truth, even if it requires producer + consumer change in the same PR. kot.completedis a new topic, emitted byKitchenService.markKotComplete; consumer in analytics buildsmv_kot_throughput.kot:createdover WebSocket andKOT_CREATEDover Kafka are two different beasts: first is real-time UI push, second is durable for analytics.OrdersService.generateKOTemits both.RESERVATION_MODIFIEDis emitted on every mutation (room-move included).night.audit.completedconsumer in analytics rebuilds the daily-flash MV.- The
dispatcher.service.tsregisters each(topic, handler, deduper)triple via a singlesubscribeAndDedup(...)helper that wrapskafkaService.subscribeand theprocessed_eventstable.
P0 items:
| AP-ID | Gold-standard fix | Acceptance criteria | CLAUDE.md | Registry |
|---|---|---|---|---|
| AP-T07-002 | Wire kitchenGateway.emitKotCreated(outletId, kot) after KOT row insert in OrdersService.generateKOT. Also publish KOT_CREATED to Kafka. Add kot.completed Kafka publish on status flip to READY/SERVED. | Playwright e2e: order entry → KDS card visible in <2s on dashboard /pos/kitchen; flip to READY → status column shifts. | hmp-pos-service/CLAUDE.md orders + kitchen | _data/use-cases/uc-pos-kot-realtime.yaml |
| AP-T07-004 | Extend FOLIO_CHARGE_POSTED payload schema with cgstAmount, sgstAmount, igstAmount, customerState, placeOfSupply. Bump payload version. Consumer writes distinct CGST/SGST/IGST rows on FolioTransaction. | A POS order with state-split tax breakdown produces a folio with three tax transaction rows whose sum equals the order's tax. | hmp-pos-service/CLAUDE.md, hmp-frontdesk-service/CLAUDE.md consumers | _data/use-cases/uc-folio-tax-breakdown.yaml |
| AP-T08-005 | (also Track 1) GuestErasedConsumer in 7 services. Listed under Track 1. | Same. | Same. | Same. |
| AP-T09-002 | Subscribe analytics EventDispatcherService to night.audit.completed. Consumer rebuilds mv_daily_flash for the audit's business date. | A manual NA run on QA refreshes the daily-flash MV; verified by curl /api/reports/daily-flash. | hmp-analytics-service/CLAUDE.md consumers | _data/use-cases/uc-analytics-night-audit.yaml |
| AP-T10-001 | Compute and persist MvAdrRevpar.adr and revpar on every upsert in reservation.consumer.ts. Backfill historical rows via scripts/backfill-mv.ts --target=mv_adr_revpar. | After backfill, /api/reports/revenue/daily and /api/analytics/dashboard/metrics agree on ADR + RevPAR for any seeded date. | hmp-analytics-service/CLAUDE.md MV table | _data/use-cases/uc-analytics-adr-revpar.yaml |
P1 items: AP-T07-006 (transactionCode by orderType), AP-T10-006 (subscribe missing 4 of 9 topics + rename folio.charge.posted), AP-T05-007 (RESERVATION_MODIFIED on room-move). All apply the canonical-taxonomy + dedup pattern.
Track 5 — Frontend Real-Data + Brand Coherence
Vision: zero hardcoded business data outside *.fixture.ts test files; UI never recomputes business numbers the server can compute; one theme system per app, source-of-truth in @hmp/design-tokens; one @hmp/api-contracts typed client.
Architectural decisions:
- Dashboard
/reservations/:idFolioTable readsres.folios[].transactions[]directly.@hmp/ui-kitshipsFolioTable@2.1.0with the corrected math; the localui-kit-fixes/FolioTableshim is removed. - Dashboard
/reports/[category]/page.tsxremoves the 40-entry hardcoded fallback list. The catalog endpoint is the only source. - Admin portal
/analytics/page.tsxremoves allplatformKpis/Top tenants/Feature adoptionhardcoded blocks; newGET /analytics/admin/platform-overviewaggregates across tenants. - Mobile
dashboardApi.getMetricsURL fixed to/analytics/dashboard/metrics; X-Property-ID header injected via shared interceptor. - Tape-chart
FilterChipsRoweither wires togetCalendar({roomTypeId, floor, source, vipOnly})or is deleted. Decision: wire it; calendar gains optional filter params. - Booking-engine theme strategy: keep
direction-a/bas the architecture (six tested presets); document in branding spec; rename "KeyNova default" to one of these. Drop the unusedMidnight Sovereign / Alabaster Court / Ink & Brassreferences from product docs. (The dashboard's current default is the KeyNova theme; Ink & Brass was the previous default and is deprecated. Different repo, different design system from the booking engine.) AiSuggestionToasteither reads from a/ai/suggestionsendpoint or is feature-flagged OFF. Decision: feature-flag OFF until Phase 2.
P0 items:
| AP-ID | Gold-standard fix | Acceptance criteria | CLAUDE.md | Registry |
|---|---|---|---|---|
| AP-T03-003 | Update branding doc to declare direction-a/b as the official theme set with six presets. Remove product references to "Midnight Sovereign / Alabaster Court / Ink & Brass" for the booking engine. (Dashboard's current default is the KeyNova theme; Ink & Brass was deprecated.) | One paragraph in hmp-docs/docs/branding/booking-engine-themes.md lists the six canonical presets; release notes call it out. Dashboard root CLAUDE.md updated to reflect KeyNova as current theme. | hmp-booking-engine/CLAUDE.md themes, hmp-web-dashboard/CLAUDE.md theme | _data/branding/booking-engine-themes.yaml, _data/branding/dashboard-theme.yaml |
| AP-T10-002 | DashboardController.getMetrics queries MvAdrRevpar via the analytics service for ADR/RevPAR/today-revenue; remove the live ReservationServiceClient aggregation. Single source of truth. | /dashboard/metrics and /reports/revenue/daily agree on the same date for ADR/RevPAR. | hmp-analytics-service/CLAUDE.md dashboard module | _data/use-cases/uc-dashboard-metrics-mv.yaml |
| AP-T10-004 | Fix dashboardApi.getMetrics URL → /analytics/dashboard/metrics. Inject X-Property-ID via shared mobile axios interceptor. | Mobile manager-app dashboard tab loads metrics without 404. | hmp-mobile-apps/CLAUDE.md api-client | _data/use-cases/uc-mobile-dashboard.yaml |
| AP-T10-005 | New analytics controller for /gst/gstr3b aggregating EventStore einvoice payloads by HSN/PoS/B2B-vs-B2C. Scheduled monthly snapshot stored in MvGstr3bSnapshot. | GET /reports/gstr3b?month=2026-04 returns a JSON summary of total taxable + tax + B2B/B2C split that matches a hand-computed value from the underlying EventStore. | hmp-analytics-service/CLAUDE.md reports + new gst module | _data/use-cases/uc-gstr3b-prep.yaml |
| AP-T10-007 | Replace (dashboard)/reports/[category]/page.tsx:23-79 hardcoded list with backend catalog only. 404 on unknown category. | Visiting an unlisted report category renders a 404 page; visiting a real category renders real reports. | hmp-web-dashboard/CLAUDE.md reports module | _data/drift/dashboard-reports-catalog.yaml |
| AP-T10-008 | New /analytics/admin/platform-overview endpoint (cross-tenant aggregate). Admin portal calls it. | Admin portal /analytics page renders real numbers; demo fixtures deleted. | hmp-admin-portal/CLAUDE.md, hmp-analytics-service/CLAUDE.md | _data/use-cases/uc-admin-platform-overview.yaml |
| AP-T13-002 | getCalendar controller accepts optional roomTypeId, floor, source, vipOnly. FilterChipsRow writes to URL state and refetches. Cosmetic chips deleted; only working filters render. | Selecting "VIP only" chip refetches calendar with vipOnly=true and renders only those rows. | hmp-frontdesk-service/CLAUDE.md calendar, hmp-web-dashboard/CLAUDE.md tape chart | _data/use-cases/uc-tape-chart-filters.yaml |
P1 items: AP-T04-004 (FolioTable real data), AP-T05-004 (CTA buttons on detail page), AP-T13-005 (in-place dialog instead of redirect), AP-T13-003 (multi-room visual continuity).
Track 6 — Configuration Management & Deploy Hygiene
Vision: secrets never in compose YAML; required prod creds enforced at boot; Kubernetes CronJobs for scheduled work; one CI gate prevents drift.
Architectural decisions:
- All secrets injected via Kubernetes ExternalSecrets (AWS SSM Parameter Store backend) for QA/staging/prod. Compose files use
${VAR}placeholders that fail-loud if unset. - Boot validators in every service for required prod env:
NIC_*,RAZORPAY_*,RESAVENUE_*(channel-service),SENDGRID_API_KEY,AWS_S3_*,ENCRYPTION_KEY,JWT_PUBLIC_KEY,INTERNAL_SERVICE_TOKEN. - Night-audit scheduler promoted from in-pod cron to k8s
CronJobwith miss-detection alert (Prometheus rule). - A new
tokenctlhelm post-install hook mintsINTERNAL_SERVICE_TOKENautomatically on deploy and writes to ExternalSecrets, eliminating the recurring "503 after deploy" gotcha.
P0 items:
| AP-ID | Gold-standard fix | Acceptance criteria | CLAUDE.md | Registry |
|---|---|---|---|---|
| AP-T02-002 | Add MinIO sidecar to dev-all.yml; add real S3 buckets to QA/prod via Terraform (hmp-infrastructure/terraform/modules/storage). Add AWS_S3_BUCKET, AWS_REGION, AWS_S3_ENDPOINT (for MinIO), AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY env. Extend AssetType enum: PROPERTY_IMAGE, ROOM_TYPE_IMAGE, GUEST_ID_PHOTO, GUEST_SIGNATURE. | POST /api/assets/upload-url returns 200 with presigned URL on local + QA. | hmp-property-service/CLAUDE.md asset module, hmp-infrastructure/CLAUDE.md terraform storage | _data/use-cases/uc-property-image-upload.yaml |
| AP-T02-003 | Drag-drop image upload widget in dashboard (dashboard)/settings/property/page.tsx and room-type page. Calls POST /assets/upload-url → S3 PUT → POST /assets/process for variant generation; persists URLs into Property.images[] via PATCH. | Hotelier uploads a 5MB JPG; system generates 3 variants (thumb/medium/full); URLs persist. | hmp-web-dashboard/CLAUDE.md settings/property | _data/use-cases/uc-property-image-upload.yaml |
| AP-T02-004 / AP-CC-2 | Re-run seed-master.sh against QA RDS for tenant 1 + Property 40b29005-.... If property no longer exists, re-create via the seed. Update doc to reference the canonical property UUID. Add a "post-deploy seed verification" step to deploy-qa.sh. | After redeploy, GET /api/properties/40b29005-... returns 200 with rooms, rate plans, taxes. Smoke-test 13/0/0. | hmp-infrastructure/CLAUDE.md seeds + deploy | _data/operations/qa-redeploy-runbook.yaml |
| AP-T11-006 | Set CHANNEL_MODE=simulator in qa-aws.yml until live ResAvenue creds land. Document in deploy runbook. | docker-compose -f qa-aws.yml exec hmp-channel-service printenv CHANNEL_MODE returns simulator. | hmp-channel-service/CLAUDE.md, hmp-infrastructure/CLAUDE.md | _data/gates/channel-mode-set.yaml |
| AP-T08-002 | (also Track 1) NIC creds + EINVOICE_MOCK=false gate. | Same. | Same. | Same. |
P1 items: AP-T09-005 (NA scheduler → k8s CronJob), AP-T12-005 (override prod compose HARDWARE_MOCK=false).
Track 7 — Gateway Routing & Two-File Rule
Vision: services.config.ts is the single declarative source. proxy.controller.ts decorators are auto-generated or CI-validated against the config. No more silent 404s.
Architectural decisions:
- New CI script
scripts/validate-gateway-routing.tsreadsservices.config.tsand parsesproxy.controller.tsAST for@All('<prefix>')decorators; fails the build if any prefix exists in one but not the other. - Long-term (Phase 2): a code-generator emits
proxy.controller.tsdecorators fromservices.config.ts. This plan does not require the generator; it requires the validator. - Dead routes (
/ai/queryresolving to nothing) are deleted from both files in the same PR.
P0 items:
| AP-ID | Gold-standard fix | Acceptance criteria | CLAUDE.md | Registry |
|---|---|---|---|---|
| AP-T11-001 | Add @All('intermediary') + @All('intermediary/*') + @All('channel-stats') + @All('channel-stats/*') + @All('channel-setup') + @All('channel-setup/*') + @All('webhook-management') + @All('webhook-management/*') decorators to proxy.controller.ts. | Curl /api/intermediary/connections reaches channel-service. CI validator passes. | hmp-api-gateway/CLAUDE.md routing, hmp-channel-service/CLAUDE.md | _data/drift/gateway-decorators.yaml |
| AP-T07-003 | Drop /pos/ prefix from hmp-mobile-apps/packages/api-client/src/index.ts to match gateway routing. (Dashboard already correct — no /pos/ prefix.) | Mobile F&B order list call GET /api/orders returns 200. | hmp-mobile-apps/CLAUDE.md api-client | _data/drift/mobile-api-prefix.yaml |
| AP-CC-1 | New CI script scripts/validate-gateway-routing.ts + workflow that fails on prefix drift. Run on every PR. | A PR that adds a prefix to services.config.ts without a matching @All decorator (or vice versa) fails CI. | hmp-api-gateway/CLAUDE.md routing-rules | _data/gates/gateway-two-file-rule.yaml |
P2 items in this track: AP-T09-009 (add bare @All('night-audit')), AP-T10-013 (delete dead /ai/query).
Track 8 — Mobile Surface Decisions & Build
Vision: every Phase 1 in-scope mobile flow has a typed API contract, real-time via shared package, and Detox/Maestro e2e in CI. Out-of-scope screens are deleted or marked WIP/disabled, not left as silent stubs.
Architectural decisions:
- A formal Phase 1 Mobile Scope Decision document (
hmp-docs/docs/platform-registry/phase1-mobile-scope-decision-2026-04-28.md) lists each potential mobile surface (frontdesk, reservations, F&B, manager NA widget, kiosk self-checkin) as IN-SCOPE or DEFERRED. - IN-SCOPE surfaces: each gets a screen, a Detox/Maestro spec, and a CLAUDE.md entry.
- DEFERRED surfaces: their stub files are deleted, not left as
<PlaceholderScreen>. The decision is permanent until a future Phase. - The "frontdesk-app" referenced in the audit prompt does NOT exist; the decision must explicitly say "no frontdesk app for Phase 1" OR scaffold it under
apps/staff/with the check-in screen.
P0 items:
| AP-ID | Gold-standard fix | Acceptance criteria | CLAUDE.md | Registry |
|---|---|---|---|---|
| AP-T04-002 | Decision document. PM + Eng lead author phase1-mobile-scope-decision-2026-04-28.md. Lists each surface IN-SCOPE/DEFERRED with rationale. Approved before any mobile-related action item starts. | Document committed, signed off in PR description by PM + Eng lead. | hmp-mobile-apps/CLAUDE.md scope section | _data/decisions/phase1-mobile-scope.yaml |
| AP-T04-001 / AP-CC-2 | (Track 6) Re-seed QA. | Same. | Same. | Same. |
| AP-T05-003 | Nginx WS routes /ws/housekeeping/ → housekeeping-service:3006 and /ws/kitchen/ → pos-service:3007. Mobile + KDS clients use wss://<host>/ws/<namespace>/. Document the upgrade-headers + read-timeout values. | Curl wss://qa-host/ws/housekeeping/socket.io/?EIO=4 returns 200 from outside the cluster. | hmp-infrastructure/CLAUDE.md nginx, hmp-housekeeping-service/CLAUDE.md, hmp-pos-service/CLAUDE.md | _data/operations/nginx-ws-routing.yaml |
| AP-T06-004 | Per scope decision: build apps/staff/app/(tabs)/checkin.tsx with real /check-in/:reservationId call OR delete the placeholder + remove the tab. | If built: Detox spec covers Indian + International happy paths. If deferred: apps/staff/app/(tabs)/checkin.tsx and the tab no longer exist. | hmp-mobile-apps/CLAUDE.md apps/staff | _data/decisions/phase1-mobile-scope.yaml |
| AP-T07-003 | (Track 7) Drop /pos/ prefix. | Same. | Same. | Same. |
| AP-T10-004 | (Track 5) Mobile dashboardApi URL fix. | Same. | Same. | Same. |
P1/P2: AP-T05-008 (Detox e2e for HK realtime), AP-T11-005 (Channel-manager flow test, multiple).
Track 9 — KDS / Kiosk Rebuild
Vision: the KDS Electron app and the kiosk Electron app share @hmp/realtime-kit (WS auth + reconnect + station-account JWT) and @hmp/api-contracts (typed REST). Both apps run RS256 JWTs minted at install time per device.
Architectural decisions:
- New shared package
@hmp/realtime-kitprovidingcreateSocketClient({namespace, token, propertyId})with infinite reconnect, structured error logs, and namespace-correctness assertions. - KDS app subscribes to
/kitchennamespace (NOT/events); listens tokot:created,kot:statusChanged,order:itemReady,order:updated. - Long-lived "station-account" JWT (RS256, 30-day expiry, scope
station:kitchenorstation:kiosk) provisioned on install; rotation via admin portal. KitchenStationis a first-class model ({id, propertyId, code, displayName});MenuItem.kitchenStationIdFK;OrdersService.generateKOTgroups items per station and broadcasts onoutlet:${id}:station:${code}rooms.- Kiosk self-check-in either talks to
/api/guest-portal/self-checkin/*for real OR the kiosk Electron app is descoped and removed from Phase 1.
P0 items:
| AP-ID | Gold-standard fix | Acceptance criteria | CLAUDE.md | Registry |
|---|---|---|---|---|
| AP-T07-001 | Rewrite hmp-kds-kiosk WebSocket client to use @hmp/realtime-kit. Target /kitchen namespace; pass auth.token; subscribe to canonical events (colon-delimited). Remove VITE_USE_DEMO=true default. | KDS receives a real KOT in <2s after dashboard /pos order entry, on QA. | hmp-kds-kiosk/CLAUDE.md (rebuild section) | _data/use-cases/uc-kds-realtime.yaml |
| AP-T06-003 | Decision via the Mobile Scope doc (Track 8): build kiosk self-check-in (connect useCheckInFlow to /guest-portal/self-checkin) OR delete the kiosk-check-in flow from hmp-kds-kiosk. | If built: Playwright/Spectron e2e covers a real check-in. If deferred: useCheckInFlow.ts mock is gone. | hmp-kds-kiosk/CLAUDE.md | _data/decisions/phase1-mobile-scope.yaml |
| AP-T07-002 | (Track 4) emitKotCreated wiring. | Same. | Same. | Same. |
P1/P2: AP-T07-005 (KitchenStation model), AP-T07-007 (reconnect Infinity), AP-T07-008 (e2e).
Track 10 — Tenant Onboarding & Self-Service
Vision: a tenant signs up on the public web, receives a verified email, lands in the dashboard with a usable subscription and the OnboardingWizard reachable as ADMIN, completes property/rooms/rate-plans/tax/policy, and is ready to take a booking — all without operator intervention.
Architectural decisions:
ResetPasswordRequestDtoaccepts{email}only when the email belongs to a single tenant; ambiguous email returns the same genericIf the email exists...message but logs at WARN.- Notification dispatcher subscribes to
TENANT_CREATEDand renderswelcome-tenant+verify-emailtemplates. OnboardingControllerallows ADMIN scoped to own tenantId (not just SUPER_ADMIN).- Admin portal
(auth)/signup/page.tsxand(auth)/forgot-password/page.tsxare the public entry points. GET /billing/plansis@Public()(anon-cacheable) so prospective tenants see pricing.OnboardingServiceis rebuilt around ServiceClient (no xPrisma).
P0 items:
| AP-ID | Gold-standard fix | Acceptance criteria | CLAUDE.md | Registry |
|---|---|---|---|---|
| AP-T01-001 | Make ResetPasswordRequestDto.tenantId optional. Service resolves user by email + tenantId? ?? lookup-by-email. Non-unique email → log + generic 200 response. Unit test covers all 3 cases. | Dashboard /forgot-password POST {email} → 200 with password.reset.requested event. | hmp-auth-service/CLAUDE.md auth module | _data/use-cases/uc-forgot-password.yaml |
| AP-T01-002 | Notification dispatcher subscribes to TENANT_CREATED (handler renders welcome-tenant.hbs) and a new USER_REGISTERED for verify-email. SendGrid sandbox required (Track 6). | Signup → email arrives in MailHog (local) / SendGrid sandbox (QA) within 30s. Flow test covers it. | hmp-notification-service/CLAUDE.md dispatcher + templates | _data/use-cases/uc-tenant-welcome-email.yaml |
| AP-T01-003 | Replace @Roles('SUPER_ADMIN') with @Roles('SUPER_ADMIN','ADMIN') on every OnboardingController route, AND add a tenantId guard so ADMIN can only act on their own tenantId. | Tenant created via signup, logging in as that admin, can call /onboarding/:tenantId/property for their own tenant. | hmp-auth-service/CLAUDE.md onboarding module | _data/use-cases/uc-onboarding-admin-role.yaml |
| AP-T01-004 | Add (auth)/signup/page.tsx (calls POST /tenants/signup) and (auth)/forgot-password/page.tsx to admin portal. Real captcha. Email-only forgot. After signup, redirect to /onboarding. | Signup → email arrives → click link → land in /onboarding Step 1 with valid session. Playwright e2e covers it. | hmp-admin-portal/CLAUDE.md auth pages | _data/use-cases/uc-tenant-self-onboarding.yaml |
P1 items: AP-T01-005 (@Public() GET /billing/plans), AP-T01-007 (OnboardingService rebuild — also Track 3).
Track 11 — HK / Frontdesk Operations
Vision: every reservation mutation (date change, room change, status flip) goes through one canonical service that recomputes rate, posts folio side-effects in the same transaction (or via the outbox), and emits RESERVATION_MODIFIED.
Architectural decisions:
ReservationsService.update()becomes the single canonical mutation entry. It recomputes per-night rates × new nights when dates change. If reservation isCHECKED_INand new dates extend beyond original, it posts anEXTENSION_NIGHTfolio CHARGE in the same transaction.RoomOpsService.roomMoveandCheckInService.roomMoveconsolidate into one — keep the variant with the pricing safeguard. Paid-upgrade rate-difference posted to folio in the same transaction.- A new
RoomBlockmodel ({id, roomId, propertyId, startsAt, endsAt, reason}) replaces "set Room.status=OUT_OF_ORDER indefinitely". Cron auto-reverts onendsAt. - Tape-chart context menu gains
mark-no-showaction; extend/shorten use the existing in-place dialog (no/reservations/:idredirect). - Multi-room reservation visual continuity: a thin connector line between sibling RoomReservation bars sharing reservationId.
P0 items:
| AP-ID | Gold-standard fix | Acceptance criteria | CLAUDE.md | Registry |
|---|---|---|---|---|
| AP-T05-001 | Fix rate recalc in ReservationsService.update() — when departureDate changes, recompute Σ ratePerNight × nights for every roomReservation. Tax recalc rides off the corrected subtotal. Failing test first (reproduces 3→5 night under-bill); then fix. | Extending 3→5 nights produces totalAmount += 2×nightlyRate; flow test asserts. | hmp-reservation-service/CLAUDE.md reservations module | _data/use-cases/uc-reservation-extend-rate.yaml |
| AP-T05-002 | When reservation is CHECKED_IN and update extends nights, post folio CHARGE rows with transactionCode='EXTENSION_NIGHT' for each added night, in the same prisma.$transaction. Emit FOLIO_CHARGE_POSTED per night. Transactional outbox handles retry if Kafka is down. | Folio balance reflects extra nights immediately on extend; flow test covers. | hmp-reservation-service/CLAUDE.md, hmp-frontdesk-service/CLAUDE.md consumers | _data/use-cases/uc-reservation-extend-folio.yaml |
| AP-T09-001 | Extend rollback transactionCode filter in night-audit.service.ts:910-916 from ['ROOM'] to ['ROOM','OVERSTAY_NIGHT','NO_SHOW']. Failing test reproduces orphan; then fix. | After NA + rollback, folio has 0 leftover charges of any of the three codes. | hmp-frontdesk-service/CLAUDE.md night-audit module | _data/use-cases/uc-night-audit-rollback.yaml |
P1 items: AP-T05-005 (consolidate two room-move impls), AP-T05-006 (paid-upgrade folio post inside tx), AP-T05-004 (CTA buttons on detail page), AP-T13-003 (multi-room continuity), AP-T13-004 (no-show action), AP-T13-005 (in-place dialog), AP-T13-006 (date-range RoomBlock).
Track 12 — Property Onboarding (typed schema sweep)
Vision: every property field a hotelier sets is a typed column with a validator; pictures live in a relational PropertyImage table; geocoding runs automatically.
Architectural decisions:
PropertyImagemodel ({id, propertyId, url, variantUrls Json, caption?, sortOrder, isPrimary});Property.images Jsonis migrated and dropped.- New
Amenitymodel ({id, tenantId, code, label, category, icon}) per tenant; RoomTypeamenityIds[]references it; UI uses dropdown. - Geocoding runs on address change via
@hmp/integrations/google-mapsadapter; populateslatitude/longitude/googleMapsUrl. TaxConfiguration.inclusive Boolean @default(false)column.CancellationPolicy.rulesJSON validated against an embedded JSON-schema document at API boundary.RatePlanRateseeded for at least the next 90 days for each rate plan in seeds.
P0 items: AP-T02-001 (lat/lng — Track 3), AP-T02-002 (S3 — Track 6), AP-T02-003 (image widget — Track 6).
P1 items: AP-T02-005 (inclusive Boolean), AP-T02-006 (cancellation rules schema). P2: AP-T02-007 (geocoding stub — promote earlier if hotelier UX needs it), AP-T02-008 (amenity master).
Track 13 — WebSocket RS256 + Resilience
(Largely covered by Track 2 AP-CC-3 and Track 8 AP-T05-003. Items here are additional resilience polish.)
Architectural decisions:
- Shared
@hmp/realtime-kitpackage (Track 9) provides client-side reconnect-Infinity, token refresh on reconnect, structured error events. - Server-side WebSocket gateways verify
tenantIdandpropertyIdon everyjoinXevent; failure disconnects. - Token-refresh propagation: when access-token rotates, the client re-auths over the same socket via a new
refreshTokenevent before disconnect.
P0 items: AP-CC-3 (Track 2), AP-T05-003 (Track 8). P1 items: AP-T07-007 (kiosk reconnect Infinity).
Track 14 — Data Quality / QA Parity
Vision: local Docker, AWS QA, AWS staging, AWS prod all converge on the same schema, the same seed contract, the same secret-injection mechanism, and the same INTERNAL_SERVICE_TOKEN automation.
Architectural decisions:
seed-master.shis idempotent and self-verifying: every seed phase ends with a row-count assertion; failures abort.deploy-qa.shends with aprisma migrate statuscheck + a smoke test of the 13 happy-path flow tests.INTERNAL_SERVICE_TOKENminted bytokenctlon every deploy; ExternalSecrets propagates it; expiry monitor alerts 7 days before.- Local seed creates
admin@keynovahotels.comANDadmin@hmphotel.com(both work) to remove cred drift. - A nightly
backfill-mv.tsk8s CronJob self-heals MV staleness. - A weekly
prisma db pullagainst QA RDS produces a drift report committed tohmp-docs/docs/platform-registry/drift-snapshots/.
P0 items: AP-T04-001 / AP-T02-004 / AP-CC-2 (re-seed QA — Track 6). P1/P2 items: AP-T09-008 (sync admin creds), AP-T10-003 (QA empty-MV root cause), AP-T10-012 (nightly backfill cron).
Track 15 — Test Coverage & Quality Gates
Vision: every business flow has a flow test; every UI has Playwright/Detox; every Kafka producer has a contract test; CI rejects PRs that drop coverage on touched files.
Architectural decisions:
flow-tests/directory underhmp-infrastructure/testing/flow-tests/is the single home for cross-service business-flow tests.- Per-service unit tests stay in
<service>/test/; per-frontend Playwright stays in<frontend>/e2e/. - Mobile e2e (Detox / Maestro) lives under
hmp-mobile-apps/apps/<app>/e2e/. - A new
flow-test-coverage.yamlregistry file lists every business flow and its test files; CI enforces that every flow has at least one entry. - Codecov per-PR delta cannot drop below -2%.
P0 items: none specific (covered transversely by every other track's test obligation). P1 items: AP-T11-005 (channel-manager flow test). P2 items: AP-T04-010 (reservation-browse), AP-T07-008 (KDS realtime e2e), AP-T13-010 (tape-chart drag/drop e2e), AP-T05-008 (mobile HK realtime Detox), AP-T09 flow-test bundle.
Part 4 — Sequenced Roll-out (refined from the audit's Section F)
Wave 1 and Wave 2 close every P0. Wave 3 closes every P1. Wave 4 is post-launch.
Wave 1 — independent P0s (≈1 week wall-clock; up to 8 parallel agents)
Each row below is one PR/branch. Items with no (blocks: …) are leaf-independent and can ship any time.
| Order | AP-ID | Track | Owner role | Blocks |
|---|---|---|---|---|
| W1.01 | AP-CC-1 | 7 | Platform Eng | AP-T11-001 may proceed in parallel; CI gate ensures no future drift |
| W1.02 | AP-T11-001 | 7 | Platform Eng | AP-T11-002 (Wave 2) |
| W1.03 | AP-T07-003 | 7 | Mobile Eng | nothing |
| W1.04 | AP-T13-001 | 3 | Frontdesk Eng | nothing |
| W1.05 | AP-T03-001 | 3 | Reservation Eng | AP-T03-002 (Wave 2) |
| W1.06 | AP-T05-001 | 11 | Reservation Eng | AP-T05-002 |
| W1.07 | AP-T05-002 | 11 | Reservation Eng | nothing |
| W1.08 | AP-T09-001 | 11 | Frontdesk Eng | nothing |
| W1.09 | AP-T07-002 | 4 | POS Eng | AP-T07-001 (Wave 2) |
| W1.10 | AP-T08-001 | 1 | Frontdesk Eng | AP-T08-006 (P1) follows |
| W1.11 | AP-T10-001 | 4 | Analytics Eng | AP-T10-002 |
| W1.12 | AP-T10-002 | 5 | Analytics Eng | nothing |
| W1.13 | AP-T10-004 | 5 | Mobile Eng | nothing |
| W1.14 | AP-T01-001 | 10 | Auth Eng | AP-T01-004 (Wave 2) |
| W1.15 | AP-T01-002 | 10 | Notification Eng | AP-T01-004 (Wave 2) — needs SendGrid (AP-CC-5 Wave 2) |
| W1.16 | AP-T01-003 | 10 | Auth Eng | nothing |
| W1.17 | AP-T02-001 | 3 | Property Eng | nothing |
| W1.18 | AP-T13-004 | 11 | Dashboard Eng | nothing |
| W1.19 | AP-T05-003 | 8 | Infra Eng | mobile WS reachability on QA |
| W1.20 | AP-CC-4 | 6 | Infra Eng | AP-T08-002 (Wave 2) |
| W1.21 | AP-T04-002 | 8 | PM + Eng lead | every mobile-related Wave 2 item |
| W1.22 | AP-T04-001 / AP-CC-2 | 6 | Infra Eng | unblocks T04, T10, T11, T12 data |
Wave 2 — dependent P0s (≈2 weeks wall-clock)
| Order | AP-ID | Track | Depends on |
|---|---|---|---|
| W2.01 | AP-T01-004 | 10 | AP-T01-001, AP-T01-002, AP-CC-5 |
| W2.02 | AP-CC-5 (SendGrid) | 6 | AP-CC-4 |
| W2.03 | AP-T02-002 | 6 | nothing else; unblocks T02-003, T06-005 |
| W2.04 | AP-T02-003 | 6 | AP-T02-002 |
| W2.05 | AP-T03-002 | 3 | AP-T03-001 |
| W2.06 | AP-T06-002 | 1 | nothing else; needs migration discipline |
| W2.07 | AP-T06-003 / AP-T06-004 | 8 | AP-T04-002 (decision) |
| W2.08 | AP-T07-001 | 9 | AP-T07-002 |
| W2.09 | AP-T08-002 | 1 | AP-T08-001, AP-CC-4 |
| W2.10 | AP-T08-003 | 1 | nothing |
| W2.11 | AP-T08-004 | 1 | nothing |
| W2.12 | AP-T08-005 | 1, 4 | nothing |
| W2.13 | AP-T09-002 | 4 | nothing |
| W2.14 | AP-T10-003 | 14 | AP-CC-2 (re-seed) |
| W2.15 | AP-T10-005 | 5 | AP-T10-003 |
| W2.16 | AP-T11-002 | 11 | AP-T11-001 |
| W2.17 | AP-T11-003 | 11 | AP-T11-002 |
| W2.18 | AP-T12-001 | 2 | AP-CC-2 |
| W2.19 | AP-T12-002 | 2 | AP-T12-001 |
| W2.20 | AP-T13-002 | 5 | AP-T13-001 |
| W2.21 | AP-CC-3 | 2, 13 | nothing |
| W2.22 | AP-T01-007 | 3, 10 | the OnboardingService rebuild |
| W2.23 | AP-T06-001 | 1 | AP-T06-002 |
| W2.24 | AP-T06-009 | 1 | nothing |
| W2.25 | AP-T10-007 | 5 | AP-T10-005 |
| W2.26 | AP-T10-008 | 5 | nothing |
After Wave 2, every P0 in the audit's Section E is closed.
Wave 3 — P1 (≈3 weeks)
The 41 P1 items in the audit's Section E. Group by track owner (most are Frontdesk + Reservation + Dashboard + Analytics).
Wave 4 — P2 (post-launch backlog)
The 39 P2 items, all track-tagged.
Part 5 — Agent Dispatch Pattern
Every fix runs as one agent (for small S-effort items) or one focused multi-step agent (for M/L items). The dispatch prompt MUST contain:
You are implementing AP-<id> per the gold-standard fix plan at
hmp-docs/docs/platform-registry/property-1-gold-standard-fix-plan-2026-04-28.md.
You MUST read Part 1 (gold-standard charter), Part 2 (mandatory deliverables),
and the relevant Track section before writing any code.
Hard rules:
- Fix the root cause, delete the workaround.
- No silent fallbacks. Production-mode missing config = boot failure.
- Schema-first: typed columns, not JSON blobs.
- One implementation, not two. Delete duplicates.
- Idempotency at every boundary.
- Tests before fix (TDD). Failing test commit, then fix commit.
- Cross-service via ServiceClient + receiver-side assertPropertyTenant.
- Update CLAUDE.md and platform-registry in the same PR.
- Migration: nullable → backfill → NOT NULL, three deploys.
Acceptance criteria for this AP-id: <copy from the per-item row>.
Files to touch: <copy from the per-item row>.
CLAUDE.md target: <copy>.
Registry target: <copy>.
Branch: audit/AP-<id>-<slug>
Commit format: audit(AP-<id>): <imperative summary>
Begin.
For multi-item bundles (e.g. all of Track 4's eventing P0s), use a parent agent that dispatches per-item children, with a track-level review at the end.
Part 6 — Ledger
This section is the live status tracker. Every PR that closes an AP-id flips the row.
| AP-ID | Track | Wave | Status | Owner | Branch | Merge SHA | CLAUDE.md updated? | Registry entry? |
|---|---|---|---|---|---|---|---|---|
| AP-T01-001 | 10 | W1 | DONE | agent | master | hmp-auth-service | yes | yes |
| AP-T01-002 | 10 | W1 | DONE | agent | master | 77e32bb (notification-service) | yes | yes |
| AP-T01-003 | 10 | W1 | DONE | agent | master | hmp-auth-service | yes | yes |
| AP-T01-004 | 10 | W2 | DONE | agent | master | hmp-auth-service | yes | yes |
| AP-T01-005 | 10 | W4 | DONE | agent | master | hmp-auth-service | yes | yes |
| AP-T01-007 | 3, 10 | W2 | DONE | agent | master | hmp-auth-service | yes | yes |
| AP-T02-001 | 3 | W1 | DONE | agent | master | hmp-property-service | yes | yes |
| AP-T02-002 | 6 | W2 | DONE | agent | master | hmp-infrastructure | yes | yes |
| AP-T02-003 | 6 | W2 | DONE | agent | master | hmp-web-dashboard | yes | yes |
| AP-T02-004 / AP-CC-2 | 6, 14 | W1 | OPEN (deferred to Phase 2) | |||||
| AP-T02-005 | 3 | W4 | DONE | agent | master | hmp-property-service | yes | yes |
| AP-T02-006 | 3 | W4 | DONE | agent | master | hmp-property-service | yes | yes |
| AP-T03-001 | 3 | W1 | DONE | agent | master | hmp-reservation-service | yes | yes |
| AP-T03-002 | 3 | W2 | DONE | agent | master | hmp-reservation-service | yes | yes |
| AP-T03-003 | 5 | W1 | DONE | agent | master | hmp-docs | yes | yes |
| AP-T03-004 | 3 | W4 | DONE | agent | master | hmp-reservation-service + hmp-web-dashboard + hmp-infrastructure (seeds) | yes | n/a (re-audit finding, not in original plan) |
| AP-T04-001 | 14 | W1 | (covered by AP-CC-2) | |||||
| AP-T04-002 | 8 | W1 | DONE | agent | master | hmp-docs (decision doc) | yes | yes |
| AP-T04-003 | 4 | P1 | DONE | agent | master | c7a8acb (reservation-service) | yes | yes |
| AP-T04-004 | 4 | P1 | DONE | agent | master | 5142488 (web-dashboard) | yes | yes |
| AP-T05-001 | 11 | W1 | DONE | agent | master | hmp-reservation-service | yes | yes |
| AP-T05-002 | 11 | W1 | DONE | agent | master | hmp-reservation-service | yes | yes |
| AP-T05-003 | 8 | W1 | DONE | agent | master | hmp-infrastructure | yes | yes |
| AP-T05-007 | 11 | W4 | DONE | agent | master | hmp-reservation-service | yes | yes |
| AP-T06-001 | 1 | W2 | DONE | agent | master | hmp-shared-packages | yes | yes |
| AP-T06-002 | 1 | W2 | DONE | agent | master | hmp-guest-service | yes | yes |
| AP-T06-003 | 8, 9 | W2 | OPEN (decision-gated — mobile scope deferred Phase 2) | |||||
| AP-T06-004 | 8 | W2 | OPEN (decision-gated — mobile scope deferred Phase 2) | |||||
| AP-T06-009 | 1 | W2 | DONE | agent | master | 12 services + infra | yes | yes |
| AP-T07-001 | 9 | W2 | DONE | agent | master | hmp-pos-service | yes | yes |
| AP-T07-002 | 4 | W1 | DONE | agent | master | hmp-pos-service | yes | yes |
| AP-T07-003 | 7, 8 | W1 | DONE | agent | master | hmp-mobile-apps | yes | yes |
| AP-T07-004 | 4 | W4 | DONE | agent | master | hmp-pos-service | yes | yes |
| AP-T07-006 | 4 | W4 | DONE | agent | master | hmp-pos-service | yes | yes |
| AP-T08-001 | 1 | W1 | DONE | agent | master | hmp-frontdesk-service | yes | yes |
| AP-T08-002 | 1, 6 | W2 | DONE | agent | master | hmp-frontdesk-service | yes | yes |
| AP-T08-003 | 1 | W2 | DONE | agent | master | hmp-frontdesk-service | yes | yes |
| AP-T08-004 | 1 | W2 | DONE | agent | master | hmp-guest-service | yes | yes |
| AP-T08-005 | 1, 4 | W2 | DONE | agent | master | 8 services | yes | yes |
| AP-T08-006 | 1 | W1 | DONE | agent | master | hmp-frontdesk-service | yes | yes |
| AP-T09-001 | 11 | W1 | DONE | agent | master | hmp-frontdesk-service | yes | yes |
| AP-T09-002 | 4 | W2 | DONE | agent | master | hmp-analytics-service | yes | yes |
| AP-T09-004 | 11 | W4 | DONE | agent | master | hmp-property-service | yes | yes |
| AP-T09-005 | 11 | W4 | DONE | agent | master | hmp-infrastructure | yes | yes |
| AP-T10-001 | 4 | W1 | DONE | agent | master | hmp-analytics-service | yes | yes |
| AP-T10-002 | 5 | W1 | DONE | agent | master | hmp-analytics-service | yes | yes |
| AP-T10-003 | 14 | W2 | OPEN (deferred to Phase 2) | |||||
| AP-T10-004 | 5 | W1 | DONE | agent | master | hmp-mobile-apps | yes | yes |
| AP-T10-005 | 5 | W2 | DONE | agent | master | hmp-analytics-service | yes | yes |
| AP-T10-006 | 5 | W4 | DONE | agent | master | hmp-analytics-service | yes | yes |
| AP-T10-007 | 5 | W2 | DONE | agent | master | hmp-analytics-service | yes | yes |
| AP-T10-008 | 5 | W2 | DONE | agent | master | hmp-analytics-service + admin-portal | yes | yes |
| AP-T10-012 | 5 | W4 | DONE | agent | master | hmp-analytics-service | yes | yes |
| AP-T11-001 | 7 | W1 | DONE | agent | master | hmp-api-gateway | yes | yes |
| AP-T11-002 | 6, 11 | W2 | DONE | agent | master | hmp-channel-service | yes | yes |
| AP-T11-003 | 11 | W2 | DONE | agent | master | hmp-channel-service | yes | yes |
| AP-T11-006 | 11 | W2 | DONE | agent | master | hmp-channel-service | yes | yes |
| AP-T12-001 | 2 | W2 | DONE | agent | master | hmp-infrastructure | yes | yes |
| AP-T12-002 | 2 | W2 | DONE | agent | master | hmp-admin-portal | yes | yes |
| AP-T12-003 | 2 | W4 | DONE | agent | master | hmp-payment-service | yes | yes |
| AP-T12-004 | 2 | W4 | DONE | agent | master | hmp-payment-service | yes | yes |
| AP-T12-005 | 2 | W4 | DONE | agent | master | hmp-infrastructure | yes | yes |
| AP-T13-001 | 3 | W1 | DONE | agent | master | hmp-frontdesk-service | yes | yes |
| AP-T13-002 | 5 | W2 | DONE | agent | master | hmp-web-dashboard | yes | yes |
| AP-T13-004 | 11 | W1 | DONE | agent | master | hmp-web-dashboard | yes | yes |
| AP-CC-1 | 7 | W1 | DONE | agent | master | hmp-api-gateway | yes | yes |
| AP-CC-3 | 2, 13 | W2 | DONE | agent | master | multiple services | yes | yes |
| AP-CC-4 | 6 | W1 | DONE (gap patched re-audit 2026-04-29) | agent | 6681da7 (infra) | hmp-infrastructure | yes | yes |
| AP-CC-5 | 1, 6, 10 | W2 | DONE | agent | master | hmp-notification-service | yes | yes |
Summary as of 2026-04-29 (re-audit): 47/47 P0 items DONE (3 deferred to Phase 2: AP-T02-004/AP-CC-2, AP-T06-003, AP-T06-004). 35/41 P1 items DONE. Audit verdict: CONDITIONAL GO — NIC IRP e-invoice credentials must be provisioned before production launch.
Re-audit 2026-04-29 code-verified spot-checks (8/8 pass; 1 gap found and closed):
- AP-CC-4 gap:
EINVOICE_MOCK: "true"was left instaging-aws.yml+prod-aws.yml. Patched commit6681da7. CI grep gate now passes. - AP-T08-001 IRN PDF reads from
folio.irn— VERIFIED (invoice-pdf.service.ts) - AP-T13-001
assertPropertyTenantinCalendarService.getCalendar— VERIFIED (line 104) - AP-T05-001 rate recalc on extend — VERIFIED (reservation-service lines 1429-1476)
- AP-T07-002
emitKotCreatedwired ingenerateKOT— VERIFIED (WebSocket + Kafka both emitted) - AP-T11-001 gateway
@Alldecorators for channel/intermediary routes — VERIFIED (all 5 prefixes) - AP-T10-001 ADR/RevPAR written in
ReservationConsumer.upsert— VERIFIED - AP-T01-001
tenantIdis@IsOptional()onResetPasswordRequestDto— VERIFIED
Re-audit 2026-04-29 (Sunita Joshi tax=0 follow-up): AP-T03-004 OTA tax double-charge + folio preview tax derivation
- Reported: front-desk staff observed "Full Payment Received" on a 29 Apr 2026 OTA reservation (Sunita Joshi, MAKEMYTRIP) but the folio preview tax line showed ₹0 despite a non-zero
taxAmountfield on the reservation row. - Root causes (4 stacked bugs):
ChannelBookingReceivedConsumer.handle()passedevent.totalAmount(the gross OTA amount, GST-inclusive) asrateAmount, thenreservations.service.create()added 12 / 18% GST on top — silent double-charge ⇒ artificial outstanding balance at front desk.- No reconciliation between OTA-asserted total and system-computed total — drift went undetected.
- KeyNova reservation seed wrote
totalAmount = base × nights(excluding tax) andpaidAmount = totalAmount, which violated the API's INCLUSIVE convention (reservations.service.ts:1116-1118writestotalAmount = base + tax). - Dashboard check-in folio preview derived tax as
totalAmount − rate × nights, which silently produced ₹0 for any reservation row using the seed (legacy) convention.
- Fix:
RoomReservationDto.isTaxInclusiveflag + reverse-derivation inreservations.service.create().CreateReservationDto.expectedTotalAmountreconciliation guard witherrorCode: 'OTA_TOTAL_MISMATCH'(±₹2 tolerance).ChannelBookingReceivedConsumersets both flags; OTA totals now flow through correctly.- Dashboard
folio-preview.ts(extracted, unit-tested) readsreservation.taxAmountexplicitly. - Seed
seed-keynova-reservations.sqlappended with idempotent normalisation block (per-night × nights vstaxAmountdetection).
- Tests: 5 new unit tests in
reservations.service.spec.ts, 3 inchannel-booking-received.consumer.spec.ts, 7 infolio-preview.test.ts. - Branch:
audit/AP-T03-004-ota-tax-and-folio-preview(TBD).
(Remaining open P1 items: AP-T09-003 night audit PDF, AP-T13-003 tape chart multi-room continuity, AP-T13-005 in-place dialog, AP-T13-006 date-range block UI. P1 ledger continues in phase1-action-plan.md.)
Part 7 — Documentation Update Protocol
When you close any AP-id, perform these doc updates in the same PR as the code change:
Per-service root CLAUDE.md
- Update the relevant table row (endpoints, schema, modules, gotchas, consumers).
- Add a new numbered Gotcha if the fix uncovers a non-obvious behaviour.
- Update the "Audit Ground Truth" date at the bottom.
Per-folder CLAUDE.md
- If a
CLAUDE.mdexists in the directly affected directory, update it to reflect the new state.
Platform registry (hmp-docs/docs/platform-registry/_data/)
- Use cases:
_data/use-cases/uc-<flow>-<feature>.yaml— owner, status, tests, compliance. - Drift:
_data/drift/<finding>.yaml— flipstatus: open → closedwith merge SHA. - Gates:
_data/gates/<gate>.yaml— for new CI gates. - Compliance:
_data/compliance/<area>.yaml— for compliance items. - Decisions:
_data/decisions/<decision>.yaml— for scope decisions like Phase 1 mobile.
Architecture docs (hmp-docs/docs/architecture/)
- New
<pattern>.mdif the fix establishes a reusable pattern (e.g.realtime-kit-package.md,idempotency-keys.md). - Update
cross-service-communication.mdif ServiceClient surface changes.
Operations runbooks (hmp-docs/docs/operations/)
- Required for: vendor onboarding (NIC IRP, ResAvenue, Razorpay, SendGrid), seed/deploy procedures, incident response.
CHANGELOG / release notes
- Add a one-line entry under the upcoming release section in the affected service's
CHANGELOG.md.
Audit ledger (this document, Part 6)
- Flip the row to
DONEwith the merge SHA, owner, branch, "yes" for both doc columns.
End of Grand Plan.