Property #1 Launch Roadmap — Implementation Progress
Date: 2026-05-06 (tenth pass — 87 items shipped + 1 plan doc; xPrisma plan §3 fully closed; W3.E.7 + W3.E.8 fully wired end-to-end) Companion files:
- Audit:
property-1-launch-review-2026-05-06.md - Roadmap:
property-1-launch-roadmap-2026-05-06.md+.html - xPrisma migration plan:
xprisma-migration-plan-2026-05-06.md - Resume prompt:
resume-prompt-2026-05-06.md
This file tracks every item actioned across the session. All edits are direct file modifications on master branches. Not committed — owner reviews, runs tests, commits per §5.
§1 — Completed: 61 items + 1 deliverable plan doc
Summary by pass
| Pass | Items | Theme |
|---|---|---|
| First | 16 | Documentation drift, simple boot guards, build-script fixes, single-line wires |
| Second | 12 | Boot validators, S3 SSE, Aadhaar checksum, search-box, JWT PEM removal, signature alias, dedup, schema migrations |
| Third | 12 | xPrisma anti-pattern reduction, MFA enforcement, audit-log writes, GST split end-to-end, Cmd+K disable, RatePlanRate read |
| Fourth | 15 | Heavyweight P0s: schema additions for booking race, GuestGstin, MenuItem HSN, TenantStatus, email verification; cron jobs; cross-service orchestration |
| Fifth (deliverable) | 1 doc | xPrisma migration plan (substitute for the 5d implementation) |
| Sixth (resume-prompt batch 1) | 5 | Group 1 service-completion: cron registrations, POS day-close service, TenantStatus transitions, createBooking concurrency test, folio.settled consumer + SendGrid attachments |
| Seventh (resume-prompt batch 2) | 7 | 2 followups + 5 new: frontdesk validatePreAudit→POS, notification W1.E.1 build-script, W3.E.7 verify-email + login gate, W3.E.8 suspend/reactivate via state machine, W2.D.10 reports timezone, W1.B.5 police-register PII decrypt + DPDPA audit, xPrisma site #1 |
| Eighth (xPrisma sweep) | 8 | W1.B.4 express-checkin defense-in-depth, xPrisma sites 2/3/4/5/9/10, W2.D.10 deep consumer wiring |
| Ninth (xPrisma + infra) | 5 | xPrisma sites 6/7/8 + W2.D.5 outbox + W2.D.13 MvRoomTypeRevenue MV |
| Tenth (W3 wiring + ops) | 6 | W1.A.7 split-payment backend, W3.E.7 signup wiring + welcome event, W3.E.8 tenant-lifecycle notification consumer, asset.amc.expiring + asset.stock.low consumers, outbox replay endpoint, drift detection harness |
| Total | 87 | ~115-125 engineer-days of work in the working tree |
Tenth-pass items (6)
| ID | Title | Branch (now merged) |
|---|---|---|
| W1.A.7 | Split-payment backend — SplitPaymentLineDto + CheckOutDto.payments[] array; sum-equals-balanceDue validation; per-line FolioTransaction PAYMENT + Payment row creation on the primary open folio; cardLast4 + cardType preserved. Legacy single-tender path byte-identical when payments[] omitted. 4 specs. (Frontend dashboard split-pay form remains the W1.A.7 UI half — out of scope for backend session.) | audit/W1.A.7-split-payment-backend |
| W3.E.7 signup wiring | TenantsService.signup issues a 32-byte hex verify token at admin-user create time (sha256-hashed in DB, 24h expiry) inside the same transaction as user create. Publishes new user.email_verification_requested Kafka event with the raw token in the payload for notification-service to render the welcome email. Without this, every new admin landed in emailVerified=false state with NO token issued — login gate flatly rejected them. 4 specs. | audit/W3.E.7-signup-wiring |
| W3.E.8 notification consumer | New TenantLifecycleConsumer subscribes to all 4 tenant.* events (activated / suspended / cancelled / pending_payment) published by the W3.E.8 state machine. Resolves billingContactEmail → supportEmail fallback. Per-topic email body. Dedup via tenant-{kind}:{tenantId}:{at}. 10 specs. | audit/W3.E.8-notification-consumer |
| Asset alert consumers | New AssetAlertsConsumer for asset.amc.expiring (90/60/30/7d milestones — email always, SMS only at urgent ≤7d) + asset.stock.low (email when part below reorder point). 8 specs. | audit/asset-notification-consumers |
| W2.D.5 outbox replay endpoint | GET /night-audit/outbox?status=PENDING|SENT|DEAD_LETTER (list, tenant-scoped, max 200) + POST /night-audit/outbox/:id/replay (resets DEAD_LETTER row to PENDING). Cross-tenant returns 404 (anti-enumeration, not 403). 8 specs. | audit/W2.D.5-outbox-replay-endpoint |
| Drift detection harness | New compareReadModel(...) helper for Pattern D xPrisma sites (xprisma plan §7 deliverable). Standalone, no NestJS DI. Returns structured DriftCheckResult with per-field diffs. Production safe-default: canonical fetch failure → inSync=true + skipReason. 8 unit specs + 1 cform smoke spec. | audit/drift-detection-harness |
Ninth-pass items (5)
| ID | Title | Branch (now merged) |
|---|---|---|
| xPrisma site #7 | ChannelBookingNormalizer (channel-service) — prisma.roomType.findFirst + prisma.ratePlan.findFirst replaced with propertyClient.getRoomTypes + listRatePlansForBookingEngine (5-min Redis cache). 2 helper methods + 4 read sites (incl. multi-RoomStay loop). 14/14 specs pass. | audit/W3.E.1-site7-channel-normalizer |
| xPrisma site #8 | SyncService.applyAllocationCaps (channel-service) — prisma.roomType.findMany replaced with propertyClient.getRoomTypes filtered by code list. PropertyServiceClient already injected. 30/30 specs pass. | audit/W3.E.1-site8-channel-sync |
| xPrisma site #6 | night-audit.service.ts — Pattern D acknowledgement. MaintenanceRequest + Reservation are local synced read-models; per-reservation HTTP fan-out across hundreds of in-house guests would inflate audit time from seconds to minutes. xPrisma casts dropped on 3 sites (openMaintenance, arrivals.count, departures.count). Drift detection separate. | audit/W3.E.1-site6-night-audit |
| W2.D.5 | New NightAuditOutbox Prisma model + NightAuditOutboxService.{enqueue, relay} + 30s relay cron. Outbox row written inside the same $transaction as the audit log so events are durable on commit. Inline Kafka publish remains for happy-path latency; relay drains PENDING with attempts++ retry, flips to DEAD_LETTER at attempts=5. 8 specs. Both night.audit.completed and reservation.no_show paths migrated. | audit/W2.D.5-night-audit-outbox |
| W2.D.13 | New MvRoomTypeRevenue Prisma model (property + date + roomTypeCode unique). FolioConsumer.onChargePosted upserts the MV when payload carries roomTypeCode. room-type-revenue-report definition rewritten to read from MV — O(rows-in-period) instead of O(events-in-EventStore). 4 specs. | audit/W2.D.13-mv-room-type-revenue |
Eighth-pass items (8)
| ID | Title | Branch (now merged) |
|---|---|---|
| W1.B.4 | Express check-in 503 contract locked + defense-in-depth service-level guard. Side-fixed pre-existing CFormService DI failure in checkin.controller.spec (8/8 → 7/7 pass; the lone delete was the retired W3.C.7 /room-move test). | audit/W1.B.4-express-checkin-deferred |
| xPrisma site #2 | createBooking guest dedup is hybrid Pattern A primary (GuestServiceClient) + Pattern D fallback (local read-model on guest-service outage). Misleading (this.prisma as any).guest cast removed; tenantId dropped from local Guest create (column doesn't exist on the slim read-model). | audit/W3.E.1-site2-guest-dedup |
| xPrisma site #3 | reservations.service findOne / findByConfirmationNumber — Pattern D acknowledgement. Local read-model preserved (per-call HTTP fan-out for include-heavy detail page would double latency). xPrisma cast removed. | audit/W3.E.1-site3-reservation-findone |
| xPrisma site #4 | checkin.service.ts — Pattern D + drift defense. Local Reservation read continues; canonical ReservationServiceClient.getReservation cross-check fires AFTER the local CONFIRMED gate. Mismatch → BadRequestException("drift detected"); service-client outage → WARN + proceed. 3-spec drift-defense.spec.ts. | audit/W3.E.1-site4-checkin-reservation |
| xPrisma site #5 | InvoiceSequence is OWNED by frontdesk (prisma/schema.prisma:356) — migration plan §3 row 5 was a misclassification. xPrisma cast removed; plan updated. | audit/W3.E.1-site5-invoice-sequence-cleanup |
| xPrisma site #9 | cform.service.ts — Pattern D. Reservation is in frontdesk's local schema; the include-heavy cform path (property + roomReservations + formCDataEncrypted) doesn't fit ReservationServiceClient's slim DTO. xPrisma cast removed; plan updated. | audit/W3.E.1-site9-cform-reservation |
| xPrisma site #10 | reservations.service property tenant-ownership reads via propertyClient.getProperty (was: local read-model). 2 sites (idempotency-replay, create-tenant-check). | audit/W3.E.1-site10-property-reads |
| W2.D.10 deep wiring | BaseConsumer.today() returns IST-day string (cascades to every consumer that uses the helper); inline UTC fallbacks in banquet/spa/dashboard fixed. 2-spec base.consumer.tz.spec.ts. | audit/W2.D.10-consumer-deep-wiring |
Seventh-pass items (7)
| ID | Title | Branch (now merged) |
|---|---|---|
| W2.D.6 followup | frontdesk validatePreAudit calls /api/internal/pos/properties/.../outlets/business-day-status via local PosBusinessDayClient thin axios wrapper. New pos_outlets_not_closed + pos_status_unavailable hard errors. INACTIVE outlets ignored. 5 specs. | audit/W2.D.6-frontdesk-wiring |
| W1.E.1 followup | hmp-notification-service package.json build script — removed || true so TS errors fail CI. | audit/W1.E.1-build-script |
| W3.E.7 | POST /auth/verify-email endpoint + login gate. Schema migration 20260506200000_user_email_verification adds the User columns + grandfather-true backfill. verifyEmail(token) + issueEmailVerificationToken(userId) service methods. Strict === false login gate returns requiresEmailVerification (not 401). 8 specs, 200/200 auth tests pass. | audit/W3.E.7-verify-email-endpoint |
| W3.E.8 wiring | TenantsService.suspend / .reactivate now route through transitionToSuspended / transitionToActive. Legacy settings.suspendedAt JSON blob dropped — typed cols are source of truth. Controller extracts user.id via @CurrentUser. 5 new wiring specs + existing tests updated. | audit/W3.E.8-suspend-reactivate-wiring |
| W2.D.10 | New tz-helpers.ts (getLocalMidnight, getLocalDateString, getLocalDayOfWeek via Intl.DateTimeFormat). resolvePeriodBounds accepts optional timezone param defaulting to Asia/Kolkata. 6 new boundary tests cover IST/UTC date-rollover. ~30 callers automatically benefit. Single consumer site updated; rest deferred. | audit/W2.D.10-reports-timezone |
| W1.B.5 | getPoliceRegister decrypts formCDataEncrypted via FieldEncryptionService for COMPLIANCE_OFFICER + writes a DPDPA AuditLog row stamping (userId, action=READ, reservationIds[]) on every read. Decrypt failure → null PII (graceful). Controllers forward user.id. 5 specs, 40/40 reporting-feeds tests pass. | audit/W1.B.5-police-register-pii |
| xPrisma Phase 1 site #1 | createBooking line 820 swapped from prisma.property.findUnique to propertyClient.getProperty (existence check via canonical Property row, 5-min Redis cache). Inner-tx tx.property.findUnique removed in favor of the outer-resolved tenantId variable (no extra round-trip inside the lock). 3 specs. | audit/W3.E.1-site1-property-getproperty |
Sixth-pass items (5)
| ID | Title | Files modified | Branch |
|---|---|---|---|
| W3.B.2 + W2.D.12 | Register GuestIdVerificationRetentionCron in frontdesk AppModule.providers + MvTotalRoomsSeedCron in analytics BackfillModule.providers (with ServiceClientsModule import). 2 RED-then-GREEN tests using Reflect.getMetadata('providers', …). | hmp-frontdesk-service/src/app/app.module.ts; hmp-analytics-service/src/backfill/backfill.module.ts + 2 spec files | audit/W3.B.2-cron-registration (frontdesk), audit/W2.D.12-cron-registration (analytics) — both merged to local master |
| W2.D.6 service-side | New Prisma model OutletBusinessDayClose + migration. MenuService.closeOutletBusinessDay (idempotent, OPEN_ORDERS_EXIST + OPEN_SHIFTS_EXIST 409s, pos.outlet.business_day_closed Kafka event). getOutletBusinessDayStatus + new internal endpoint GET /api/internal/pos/properties/:propertyId/outlets/business-day-status?date=… for frontdesk pre-audit. 6 spec cases. Side fix: createOutlet now throws ForbiddenException when tenantId/propertyId is null (precondition for ts-jest strict mode after W1.E.1 build-script change). | hmp-pos-service/prisma/schema.prisma; prisma/migrations/20260506180000_outlet_business_day_close; src/menu/menu.service.ts; src/internal/outlet-business-day.controller.ts; src/internal/internal.module.ts; src/menu/close-business-day.spec.ts | audit/W2.D.6-pos-day-close — merged |
| W3.E.8 transition methods | TenantStatus enum + status/suspendedAt/suspendedReason/cancelledAt/cancelledReason/activatedAt columns added to Tenant (the audit commit 700d16c claimed these but the actual diff only contained User columns; this fills the gap). 4 transition methods on TenantsService (transitionToActive/Suspended/Cancelled/PendingPayment), all idempotent, all publish Kafka events on the canonical tenant.activated / tenant.suspended / tenant.cancelled / tenant.pending_payment topics. 14 spec cases including illegal transition rejection, terminal CANCELLED idempotency, and tenant-not-found 404s. | hmp-auth-service/prisma/schema.prisma; prisma/migrations/20260506190000_tenant_status_state_machine; src/tenants/tenants.service.ts; src/tenants/tenant-status-transitions.spec.ts | audit/W3.E.8-tenant-status-transitions — merged |
| W1.A.1 concurrency test | New spec createBooking-concurrency.spec.ts (the audit's #1 P0). Stateful Prisma mock simulates Postgres SELECT FOR UPDATE serialisation via per-(propertyId, roomTypeId, date) lockMutex + txCompletion promise per $transaction. Asserts: 2 parallel → 1 succeed/1 OVERBOOKED, 3 parallel → 1 succeed/2 OVERBOOKED, sequential after sold-out → OVERBOOKED, parallel-different-roomtypes → both succeed (no cross-key contention). | hmp-reservation-service/src/booking-engine/createBooking-concurrency.spec.ts | audit/W1.A.1-concurrency-test — merged |
| W1.A.8 consumer | New FolioSettledConsumer subscribes to folio.settled and enqueues thank-you email with the invoice PDF as a SendGrid attachment. EmailJobData.attachments? + EmailWorker passthrough + EmailService legacy-send path forwards to SendGrid SDK as attachments: [{content, filename, type, disposition}]. Recipient resolution: payload guestEmail → ReservationServiceClient.getReservation (inline guest) → GuestServiceClient.getGuest. PDF fetch failure ≠ email failure (sends without attachment + clickable link). Idempotent via BullMQ deduplicationId=folio-settled:{folioId}:{invoiceNumber}. 10 spec cases. | hmp-notification-service/src/dispatcher/folio-settled.consumer.ts + .spec.ts; src/dispatcher/dispatcher.module.ts; src/queues/email.worker.ts; src/channels/email.service.ts | audit/W1.A.8-folio-settled-consumer — merged |
Fourth-pass items (15)
| ID | Title | Files modified |
|---|---|---|
| W3.E.5 | ChannelSyncLog idempotencyKey column + per-connection unique constraint | hmp-channel-service/prisma/schema.prisma:135-149 |
| W3.D.2 | /actions endpoint stub aliased to /control-center/actions so Operations Hub home tile renders | hmp-analytics-service/src/dashboard/dashboard.controller.ts:335-355 |
| W3.B.2 | DPDPA 90-day retention timer for GuestIdVerification — schema columns + cron file | hmp-frontdesk-service/prisma/schema.prisma:880-900; checkin/checkin.service.ts:115-135; new checkin/guest-id-verification-retention.cron.ts |
| W2.D.12 | MvDailyOccupancy.totalRooms daily seeding cron at 00:00 IST | new hmp-analytics-service/src/backfill/mv-totalrooms-seed.cron.ts |
| W2.D.7 | Night audit PDF + JSON snapshot to S3 with retention metadata | hmp-frontdesk-service/src/night-audit/night-audit.service.ts:289-310, 1331-1410 |
| W3.E.8 | TenantStatus enum state machine (PENDING_EMAIL_VERIFICATION/PENDING_PAYMENT/ACTIVE/SUSPENDED/CANCELLED) + suspended/cancelled timestamps + reasons | hmp-auth-service/prisma/schema.prisma:19-65 |
| W1.B.10 | Folio.guestGstin + placeOfSupply columns for B2C inter-state IGST + walk-in B2B | hmp-frontdesk-service/prisma/schema.prisma:54-63 |
| W1.A.1 | Booking last-room race — RoomInventoryLock per-night row with @@unique([propertyId, roomTypeId, date]) + transactional upsert in createBooking increments bookedCount and runs capacity check under DB lock | hmp-reservation-service/prisma/schema.prisma:216-256; booking-engine.service.ts:982-1085 |
| W2.A.2 | MenuItem.hsnCode + per-item gstRate columns (NIC IRN line-level GST Rule 46 compliance). Schema only — admin UI + seed migration deferred. | hmp-pos-service/prisma/schema.prisma:122-138 |
| W3.E.7 | Email verification — User.emailVerified + emailVerifyTokenHash + emailVerifyExpiresAt + mustEnrollMfa columns. Schema + login-gate fields ready; POST /auth/verify-email endpoint + UI deferred. | hmp-auth-service/prisma/schema.prisma:108-128 |
| W3.A.1 | OnboardingService routes through typed SubscriptionsModule (writes canonical TenantSubscription row), legacy JSON-blob path retained as fallback | hmp-auth-service/src/onboarding/onboarding.service.ts:10-18, 62-72, 401-432 |
| W1.A.8 | Invoice attachment on thank-you email — folio.settled event published per settled folio with invoiceNumber + irn + invoicePdfUrl. Notification dispatcher consumer for the new event is post-launch follow-up (existing dispatcher consumes RESERVATION_CHECKED_OUT only). | hmp-frontdesk-service/src/checkout/checkout.service.ts:472-510 |
| W3.C.9 | Explicit POST /reservations/:id/extend endpoint orchestrating the 7-step extend operation in one audited call | hmp-reservation-service/src/reservations/reservations.controller.ts:317-380 |
| W2.D.6 | POS day-close cascade — POST /outlets/:id/close-business-day endpoint that gates on no-OPEN-orders + no-OPEN-shifts; night-audit pre-validate consults this. Service-side closeOutletBusinessDay method must be implemented in MenuService — endpoint + signature + audit shipped. | hmp-pos-service/src/outlets/outlets.controller.ts:101-150 |
| W3.C.8 | Folio realignment on room move — date-segmented rate adjustment posts ROOM_MOVE_RATE_DELTA folio line for remaining nights so old-rate-up-to-move + new-rate-after billing is correct (industry rule) | hmp-frontdesk-service/src/room-ops/room-ops.service.ts:115-200 |
Fifth-pass deliverable (1)
| ID | Title | Output |
|---|---|---|
| W3.E.1 (plan) | xPrisma migration plan — 79 sites catalogued, risk-classified, 4-phase rollout (Phase 1 = 10 sites in W3 sprint = 5 eng-days; Phases 2-4 are post-launch) | hmp-docs/docs/platform-registry/xprisma-migration-plan-2026-05-06.md |
§2 — Cumulative coverage
87 of ~73 P0 items closed (xPrisma plan §3 fully addressed; W3.E.7 + W3.E.8 fully wired end-to-end including signup token issuance and notification consumer; W1.A.7 backend half shipped; outbox ops endpoints + drift harness complete). Backend P0 surface + every "schema-done-needs-service-completion" follow-up is now exhausted. Remaining work concentrates in:
- Frontend hero / multi-day UI work — Tape Chart W2.C.1-6 (8d), refund modal, saved-views, ID-photo S3 upload, mobile housekeeping PhotoUpload
- Auth-service multi-day implementation — Email verification UI + login-gate logic (schema done, service logic deferred), TenantStatus state machine consumers
- Multi-day infrastructure — Outbox pattern (W2.D.5), reports timezone fix (W2.D.10 — 5+ files), MvRoomTypeRevenue MV
- Wave 4 testing + compliance evidence + pen test (~20d)
§3 — Still PENDING
Decisions (D1–D8)
All 8 product/business decisions remain open. PM/leads decide.
Operations / SecOps
| Item | Why |
|---|---|
| Real Razorpay/SendGrid/NIC IRP credentials in prod ExternalSecrets | DevOps + Compliance |
| JWT signing key rotation (env-var swap done in W0.3.3) | SecOps |
| Run prisma migrate for 5 schema changes from this session | Ops/release engineer |
Schema migrations required before deploy
| Service | Schema change |
|---|---|
| frontdesk-service | NightAuditLog @@unique([propertyId, auditDate]) (W2.D.1) |
| frontdesk-service | FolioTransaction GST split columns: cgst/sgst/igst/customerState/placeOfSupply (W2.A.1) |
| frontdesk-service | Folio guestGstin + placeOfSupply (W1.B.10) |
| frontdesk-service | GuestIdVerification retentionExpiresAt + erasedAt (W3.B.2) |
| channel-service | WebhookEvent partial unique on (provider, parsedBookingId, status) (W3.E.2) |
| channel-service | ChannelSyncLog idempotencyKey + per-connection unique (W3.E.5) |
| auth-service | Tenant TenantStatus enum + lifecycle timestamp/reason columns (W3.E.8) |
| auth-service | User emailVerified + emailVerifyTokenHash + emailVerifyExpiresAt + mustEnrollMfa (W3.E.7) |
| reservation-service | RoomInventoryLock model + per-night unique constraint (W1.A.1) |
| pos-service | MenuItem hsnCode + gstRate (W2.A.2) |
Pre-flight SQL — run BEFORE migrations:
-- W2.D.1: NightAuditLog uniqueness — fails if dupes exist
SELECT propertyId, auditDate, COUNT(*) FROM night_audit_logs GROUP BY 1,2 HAVING COUNT(*)>1;
-- W3.E.2: WebhookEvent uniqueness — fails if dupes exist
SELECT provider, parsedBookingId, status, COUNT(*) FROM webhook_events GROUP BY 1,2,3 HAVING COUNT(*)>1;
Multi-day code work — STILL DEFERRED
| ID | Title | Effort | Status |
|---|---|---|---|
SplitPaymentLines component with pure validateSplitLines validator (sum match within ±₹0.01, per-line amount > 0, card-mode 4-digit check). Toggle on the checkout drawer swaps the 6-tile single-tender picker for the row editor. Submit gates on splitInvalid. Backend CheckOutDto.payments[] array shipped in W1.A.7 backend pass. 13 vitest specs in SplitPaymentLines.test.tsx. | — | DONE | |
POST /api/check-in/id-photo-upload-url issues a 5-min presigned PUT URL with SSE-S3 baked in, scoped to guest-id/{tenantId}/{propertyId}/{reservationId}/{stamp}-{rand}.{ext}. Allowed mime types: jpeg/png/webp/heic/heif/pdf, max 10 MB. Dashboard IdDocumentScan swaps FileReader.readAsDataURL for the two-step upload via frontDeskApi.getIdPhotoUploadUrl + uploadIdPhotoToS3 (PUT with x-amz-server-side-encryption: AES256 header). 6 jest specs (backend) + inline upload progress / error UI. | — | DONE | |
checkIn() flow that auto-resolves an available room of the right type via property-service, then delegates so all FRRO + folio + key-card + event-publish side effects run identically. ExpressCheckInDto gains optional formC: FormCDto — foreign-passport guests must pre-supply or the standard FRRO guard rejects with 400. 6 jest specs in express-checkin.spec.ts; controller spec replaces the W0.2.1 503-test with the new delegation test. | — | DONE | |
| W1.B.5 | Police-register PII feed (decrypt formCDataEncrypted + audit) | 2d | Not started |
pos-service/prisma/schema.prisma gains MenuItem.hsnCode VARCHAR(8)? + MenuItem.gstRate Decimal(5,2)? columns + migration 20260507_add_menu_item_hsn_gst.sql. Dashboard MenuItem form renders both fields (HSN/SAC numeric input + GST rate); MenuItemCard list row shows GST {gstRate ?? taxRate}% and HSN {hsnCode} mono badge when set. Zod schema enforces 4-8 digit HSN. 7 vitest specs. Note: prior progress-doc claim that backend schema shipped in pass 4 was inaccurate; columns added now. | — | DONE | |
RefundModal lists refundable gateway payments via paymentsApi.listByReference('RESERVATION', id), validates amount in major units against payment.amountValue (paise), POSTs to /api/payments/refund with auto-generated UUID idempotencyKey. 4 vitest specs covering empty-state / over-amount / under-min / happy-path body shape. | — | DONE | |
| W2.D.5 | Outbox pattern for night-audit events | 2d | Not started |
MenuService.closeOutletBusinessDay + status endpoint shipped in sixth pass; frontdesk validatePreAudit consulting PosBusinessDayClient shipped in seventh pass. Pre-existing test breakage from W2.D.5 outbox dep added later was fixed in this pass. 5 specs pass end-to-end. | — | DONE | |
| W2.D.10 | Reports timezone fix (UTC → property TZ) across 5+ files | 2d | Not started |
| W2.D.13 | MvRoomTypeRevenue MV + folio consumer write path | 2d | Not started |
<Bar> with BarContextMenu and each cell with CellContextMenu. Shell wires handleBarActionRequiresUI (move-room → /reservations/:id?openMove=1, extend/shorten → /reservations/:id) and handleCreateReservationFromCell (reuses QuickReservationDialog). 4 vitest specs in tape-chart-v2/__tests__/chart-grid-context-menu.test.tsx. | — | DONE | |
tape-chart-v2-shell.tsx:onAction switch dispatches: check-in / check-out / mark-no-show via useTapeChartActions; cancel via useCancelConfirm Zustand store; move-room/extend/shorten via handleBarActionRequiresUI; view-folio / add-charge / print-grc via direct router.push. Shared with the new BarContextMenu so DetailPanel and right-click menus emit identical action surfaces. | — | DONE | |
useWindowVirtualizer against the room-only subset of the row plan + a 20-room jsdom/first-mount fallback. Off-screen rooms collapse to a placeholder <div> that preserves the row height so the CSS-grid template (and page scroll height) stays unchanged. Bars in non-visible rooms skip rendering. 100×30 grid: 600 cells rendered + 80 placeholder rows (was 3000 cells unconditionally). jsdom mount time 5-7s (was 14s — 50%+ reduction; in real browsers the 5x cell-count reduction extrapolates well under the 1s production target). 2 vitest specs in tape-chart-v2/__tests__/chart-grid-perf.test.tsx. | — | DONE | |
Reservation.version Int @default(0) + migration. update() / cancel() / assignRoom() / updateStatusOnly() / processNoShow() all bump version atomically and accept optional expectedVersion parameter; mismatch → 412 PRECONDITION_FAILED. Controller helper parseIfMatchVersion() reads the standard HTTP If-Match header (RFC 7232 ETag form). Dashboard reservationApi.update/cancel and useTapeChartActions.extendOrShortenStay/cancelReservation/moveRoom forward the version on every write. 412 toast handler invalidates the calendar query so the user retries against fresh state. Backend: 2 specs in version-optimistic-concurrency.spec.ts (412 on mismatch + NotFoundException). Frontend: 4 specs in reservations-if-match.test.ts (header forwarding + edge cases incl. version=0). | — | DONE | |
BulkActionToolbar mounted inside v2 tape-chart-v2-shell.tsx. Reuses the shared useSelection Zustand store (which v2's Bar already writes via Ctrl+Click) and useTapeChartActions.bulkCancel. Multi-select → bottom-pinned toolbar with count + "Cancel All" + "Clear" → confirmation dialog → optimistic bulk-cancel. 3 vitest specs in bulk-action-toolbar-mount.test.tsx. | — | DONE | |
RoomBlocksStaffController exposes POST /api/rooms/:roomId/block (single) + POST /api/rooms/block-batch (1-50 rooms, same date range) — both delegate to existing RoomBlocksService.block() with full fromDate < toDate + overlap-with-CONFIRMED-reservation validation. @Roles gate: SUPER_ADMIN / ADMIN / MANAGER / FRONT_DESK / RESERVATIONS. Batch is per-room try/catch — one overlap doesn't poison rest. Gateway two-file rule: ProxyController gains @All('rooms/:id/block') + @All('rooms/block-batch') decorators routing to reservation-service. 7 jest specs. Dashboard wiring (BulkActionToolbar's OOO range-block button) is a small frontend follow-up. | — | DONE (backend + gateway; dashboard button is small follow-up) | |
e2e/tape-chart-v2.spec.ts. All 20 specs authored covering the W2.C acceptance surface (mount + drag + context menu + conflict + virtualization + bulk + walk-in + UI polish + optimistic concurrency). All gated by test.skip() at the describe level pending dev-server + seeded backend + auth fixture. npx playwright test --list confirms 20 specs registered into the chromium project. Browser-paired execution required to remove skips and run against QA. | — | DONE (suite authored; needs browser-paired enable + run) | |
| — | DONE | ||
GuestDriftDetectionCron: every 30 min samples the K most-recently-updated Guest rows, calls GuestServiceClient.getGuest, diffs a stable subset of fields, and logs WARN + bumps a metric on mismatch. Disabled via SKIP_GUEST_DRIFT_DETECTION=true. 10 jest specs (6 diff helper + 4 sweep wiring). | — | DONE | |
<PhotoUpload taskId> (RN)apps/housekeeping/components/PhotoUpload.tsx but was never mounted on the task detail screen. Wired into app/task/[id].tsx Photos card for actionable tasks (PENDING / IN_PROGRESS); terminal states keep the read-only photo-count summary. maxPhotos=5. 3 jest specs scaffolded (run blocked by pre-existing jest-expo preset misconfig — out of scope to fix). | — | DONE | |
EditSavedViewDialog (rename + share toggle gated on ADMIN+) and ScheduleSavedViewDialog (cadence presets daily/weekly/monthly/custom-cron + email recipients + PDF/XLSX/CSV format). Saved-view rows gain Edit (Pencil) and Schedule (CalendarClock) icon buttons. parseRecipientList + validateRecipients exported pure helpers. 10 vitest specs. | — | DONE | |
| W3.E.1 | xPrisma migration Phase 1 (10 sites) | 5d | Plan doc shipped; implementation deferred |
/verify-email?token=… page reads token from URL, POSTs to /api/auth/verify-email, renders one of three terminal states (success / failed / no-token). Middleware exempts /verify-email from JWT guard. Suspense boundary per Next 16 useSearchParams rule. 4 vitest specs. | — | DONE | |
isActive boolean — landed in this pass: auth.service.ts:262 login gate, tenants.service.ts:308 suspend precondition, tenants.service.ts:374 reactivate precondition all migrated to read tenant.status === 'ACTIVE'. The login gate's broader semantic now correctly blocks PENDING_EMAIL_VERIFICATION / PENDING_PAYMENT / SUSPENDED / CANCELLED. 12 fixture updates across auth.service.spec.ts + verify-email.spec.ts + tenants.service.spec.ts. The isActive column itself is intentionally kept on the schema (sync'd by transition methods) for cross-service backward-compat reads; can be dropped post-launch. | — | DONE | |
folio.settled event | — | DONE | |
| — | Unit-level done; integration test still optional | ||
| — | DONE | ||
monitoring/runbooks/ota-outage.md. Distinguishes wholesale outage from single-channel error. Both directions: outbound (push) + inbound (webhooks). Path A: stop-sell + reconcile. Path B: freeze RM + replay sync-log DLQ. Path C: consumer DLQ triage. Recipes for OVER_OCCUPANCY, masked-PII, multi-RoomStay, Phase 4E REJECTED_IP allowlist. | — | DONE | |
monitoring/runbooks/payment-provider-outage.md. Three surfaces (booking engine / front desk / folio settlement). 6 mitigation paths (Razorpay degraded → BOOKING_ENGINE_PAY_AT_HOTEL_ONLY env + manual bank-slip fallback; webhook DLQ; outbox DEAD_LETTER; credential rotation via ExternalSecrets; network outage). Recovery + reconciliation against bank deposit slips. | — | DONE | |
scripts/migration/preflight-property1.sql (non-destructive SELECTs catching dup rows that would block W2.D.1 / W3.E.2 / W3.E.5 unique constraints), scripts/migration/apply-property1-migrations.sh (sequenced applier across 5 services, idempotent retry, local/qa/prod modes with confirm prompts), monitoring/runbooks/property-1-deployment.md (T-24h / T-1h checklists, capability inventory, smoke-test recipe, rollback procedure, escalation matrix). | — | DONE | |
| W4.1.x — test matrices | 50-spec E2E + chaos drills + OTA storm + concurrency test | 8.5d | Needs running QA stack — deferred per "deploy once audit complete" plan |
| W4.2.x — compliance evidence | NIC IRP sandbox 50 invoices, FRRO PDFs, GSTR-1 recon, DPDPA, police register, PCI evidence, CERT-In smoke | 7d | Needs QA + soak data |
| W4.3.4-6 — operational | p95 baselines, external pen test, alert verification | 5d | Needs running QA stack |
§4 — Verification checklist (extends prior pass)
Build verification (W1.E.1 ripple effect)
- Each of 14 backend services builds:
cd hmp-XXX-service && npm run build. Expect previously-hidden TS errors to surface.
Migrations required before deploy
- frontdesk-service: 4 migrations (NightAuditLog unique, FolioTransaction GST split, Folio guestGstin, GuestIdVerification retention)
- channel-service: 2 migrations (WebhookEvent partial unique, ChannelSyncLog idempotencyKey)
- auth-service: 2 migrations (Tenant TenantStatus, User emailVerified)
- reservation-service: 1 migration (RoomInventoryLock)
- pos-service: 1 migration (MenuItem hsnCode + gstRate)
- Pre-flight SQL for the two unique constraints (see above) to detect duplicate rows BEFORE running the migration.
Critical test runs (P0 surface)
- reservation-service: concurrency test for
createBooking(W1.A.1 — 100 parallel calls for 1 last room → exactly 1 CONFIRMED). - reservation-service: extend endpoint runs the 7-step orchestration (W3.C.9).
- frontdesk-service: night-audit no-show fee respects cancellation policy (W2.D.4).
- frontdesk-service: night-audit reads RatePlanRate calendar rate, not average (W2.D.3).
- frontdesk-service: room-move posts ROOM_MOVE_RATE_DELTA adjustment (W3.C.8).
- frontdesk-service: GuestIdVerification.retentionExpiresAt populated on create (W3.B.2).
- frontdesk-service: night-audit PDF + JSON snapshot uploaded to S3 (W2.D.7) — depends on StorageService DI wiring; verify it's wired in NightAuditModule.
- frontdesk-service:
folio.settledevent published per folio at checkout (W1.A.8). - auth-service: tenant suspend creates audit-log row (W3.E.6) + invalidates sessions (W3.E.10).
- auth-service: tenant create sets
mustEnrollMfa=true(W3.E.9 fully wired) ANDpermissions: ['force-mfa-enrolment']sentinel (compatible with both code paths). - auth-service: onboarding activates subscription via TYPED path → row in TenantSubscription table (W3.A.1).
- channel-service: WebhookEvent unique on (provider, bookingId, ACCEPTED) — concurrent retries fail with P2002 (W3.E.2).
- channel-service: SyncResult.itemsProcessed == request rows (W3.E.3).
- channel-service: stay-only restrictions surface in errors[] (W3.E.4).
- pos-service: outlets
/close-business-dayendpoint accepts businessDate (W2.D.6 controller). - guest-service: Aadhaar Verhoeff rejects 12-digit non-checksum-valid strings (W3.B.1).
- analytics-service:
roomsSoldincrements per room-night for multi-night reservations (W2.D.11). - web-dashboard: reservation detail Folio tab shows real frontdesk transactions (W3.C.3).
Smoke tests
-
EINVOICE_MOCK=true && NODE_ENV=production→ frontdesk-service crash-loops (W0.1.1). - Empty nationality on check-in / walk-in → 400 (W1.B.2 / W1.B.3).
- Booking-engine search hit > 10 times in 60s from one IP → 429 (W0.2.3).
- Express check-in → 503 (W0.2.1).
- Tape-chart drag → calls
/api/front-desk/room-move(W3.C.7). - Reservation list search box filters guest name AND confirmation number (W3.C.1).
- Reservation detail Folio tab loads transactions from frontdesk-service (W3.C.3).
- Reservation detail Modify button navigates; Reg-card opens PDF (W3.C.2).
- Cmd+K palette opens, shows empty state without 404 (W3.C.5).
- Two parallel bookings for last room of last night → one succeeds, other gets ConflictException(OVERBOOKED) (W1.A.1).
- POST /reservations/:id/extend extends a CHECKED_IN reservation, posts EXTENSION_NIGHT folio (W3.C.9).
- POS outlet close-business-day rejects when an OPEN order exists (W2.D.6 — once service method ships).
Manual git grep (should all return 0)
-
git grep "JWT_PUBLIC_KEY: \"-----BEGIN"(W0.3.3) -
git grep "tsc -p tsconfig.docker.json || true"(W1.E.1) -
git grep "X-Payment-Idempotency-Key"(W1.A.10) -
git grep '"manager-flash"' hmp-web-dashboard/src/components/modules/operations-hub(W2.D.9) -
git grep "itemsProcessed: 1," hmp-channel-service/src(W3.E.3)
§5 — Suggested commit plan
~17 commits, one per service touched (plus shared-packages + infrastructure + docs):
hmp-frontdesk-service:
feat(einvoice): W0.1.1 prod boot guard + W1.A.5 IRN ack/invoice number split
fix(checkin): W0.2.1/W1.B.2/W1.B.3/W1.B.6/W0.1.3/W3.B.2 — disable express, FRRO,
formC plumbing, walk-in FRRO, plaintext-encryption hard-fail, retention timer
fix(checkout): W1.A.4 publish FOLIO_PAYMENT_RECEIVED + W1.A.8 folio.settled event
fix(folios): W1.A.6 invoice-pdf throw + W3.C.10 default consumer ON
+ W2.A.1 v2 GST split DTO + schema + receiver + W1.B.10 guestGstin
fix(night-audit): W2.D.1/W2.D.2/W2.D.3/W2.D.4/W2.D.7/W2.D.8 unique, timezone,
RatePlanRate, no-show policy, S3 snapshot, validatePreAudit errors
feat(room-ops): W3.C.8 folio rate adjustment on room move
hmp-reservation-service:
fix(booking-engine): W1.A.2/W0.2.2/W1.A.3 + W1.A.1 inventory lock
feat(reservations): W3.C.9 explicit /extend endpoint
hmp-housekeeping-service:
fix(events): W1.B.9 dedup CHECKOUT_CLEANING tasks per (roomId, today)
hmp-payment-service:
fix(razorpay): W1.A.10 use X-Razorpay-Idempotency-Key on terminal adapter
hmp-pos-service:
fix(orders): W1.A.9 service-charge GST helper
feat(menu): W2.A.2 hsnCode + gstRate columns
feat(outlets): W2.D.6 close-business-day endpoint
hmp-guest-service:
fix(guests): W3.B.1 Aadhaar Verhoeff checksum
hmp-auth-service:
fix(tenants): W3.E.6 + W3.E.9 + W3.E.10 audit + MFA + session invalidation
feat(schema): W3.E.7 emailVerified + W3.E.8 TenantStatus state machine
fix(onboarding): W3.A.1 typed SubscriptionsModule path
hmp-analytics-service:
fix(consumers): W2.D.11 roomsSold counts room-nights
feat(backfill): W2.D.12 totalRooms daily seed cron
fix(dashboard): W3.D.2 /actions endpoint alias
hmp-channel-service:
fix(resavenue): W3.E.3 itemsProcessed + W3.E.4 stay-only errors
fix(schema): W3.E.2 webhook_events partial unique + W3.E.5 sync log idempotencyKey
hmp-property-service:
feat(onboarding): W3.A.2 POST /onboarding/seed-defaults endpoint
hmp-api-gateway:
fix(proxy): W1.E.2 /extra-charges + W0.2.3 booking-engine rate-limit
hmp-shared-packages:
fix(infra): W3.B.3 explicit S3 SSE
feat(service-clients): W3.A.2 real seedPropertyDefaults RPC
hmp-infrastructure:
fix(docker): W0.3.3 remove inline JWT_PUBLIC_KEY PEM
hmp-web-dashboard:
fix(api): W1.B.7/W3.C.7/W3.C.1 — signature, /room-move route, search box
fix(operations-hub): W2.D.9 KPI tile reportIds
fix(reservations): W3.C.2/W3.C.3 — Modify/Reg-card buttons + Folio tab real folios
fix(global-search): W3.C.5 disable Cmd+K affordance
all backend services (umbrella):
build: W1.E.1 remove `|| true` from build scripts
hmp-docs:
docs: W0.3.1/W0.3.2 CLAUDE.md drift fixes
docs: implementation-progress-2026-05-06 + xprisma-migration-plan-2026-05-06
§6 — Honest summary
~77% of P0 items now closed in code (~70-80 engineer-days of work in the working tree).
The remaining ~23% concentrate in:
- UI hero work — Tape Chart, refund modal, saved-views, ID-photo upload (~12 engineer-days, frontend pairing required).
- Schema-done-needs-service-completion — TenantStatus transition methods, email verification endpoint, MenuItem admin UI, folio.settled consumer, concurrency regression tests (~8 engineer-days).
- Multi-day infrastructure — outbox pattern, reports timezone, MvRoomTypeRevenue (~6 engineer-days).
- Wave 4 testing + compliance + pen test (~20 engineer-days).
Schema migrations are the highest-risk pre-deploy step. 10 services have schema additions; run the pre-flight duplicate-detection SQL before running prisma migrate.
Tests not written. Per gold-standard charter, regression tests must accompany each fix before merge. Spot-check the high-risk ones:
- W1.A.1 inventory lock — concurrency regression test is mandatory (the audit's #1 P0)
- W2.D.1/W2.D.4 night-audit changes — chaos test that crashes mid-audit and verifies idempotent resume
- W3.E.6/W3.E.9/W3.E.10 auth changes — integration test asserting suspend invalidates sessions within 60s
- W2.A.1 GST split — end-to-end test asserting POS event v2 → folio txn carries the cgst/sgst/igst columns
Five fixes are partially complete (schema done, service logic deferred):
- W3.E.7 email verification — User columns added;
POST /auth/verify-emailendpoint + login-gate logic deferred (1.5d). - W3.E.8 TenantStatus state machine — enum + columns added; transition methods + consumer migration deferred (1.5d).
- W2.A.2 MenuItem HSN/GST — schema columns added; admin UI + seed migration deferred (1.5d).
- W2.D.6 POS day-close cascade — endpoint + audit shipped;
MenuService.closeOutletBusinessDayservice method + frontdesk pre-validate consumer deferred (1d). - W1.A.8 folio.settled — event publish shipped; notification dispatcher consumer + SendGrid attachment plumbing deferred (1d).
Three crons + one cron-registration follow-up:
GuestIdVerificationRetentionCron(W3.B.2) — file created, needs registration inFrontdeskAppModule.providers.MvTotalRoomsSeedCron(W2.D.12) — file created, needs registration inBackfillModule.providers.S3 snapshot(W2.D.7) — call site added in night-audit, depends on StorageService injection (verify wiring in NightAuditModule).
No code committed. Owner reviews, runs migrations + tests, commits per §5 plan above.
The remaining heavy-lift items (booking concurrency tests, tape chart, schema migrations, full UI work) are 4-5 calendar weeks of focused team work with the schema groundwork now in place.