Skip to main content

BF-006 — Night Audit (End-of-Day Batch)

Business flow (cross-EPIC user journey). Reconciled 2026-05-25 as Wave 2 of the RTM Reconciliation Scale-Out. Follows the template established by BF-001-tenant-property-onboarding.md and the playbook in ../reconciliation/PLAYBOOK.md.

Night audit is the single most destructive batch in the platform — it posts charges to every in-house folio AND advances the property's business date in the same transaction. The K8s CronJob fires at 20:30 UTC (~02:00 IST) every day; if it fails, the entire hotel's next day is blocked. Flow-9 tests on the 13-flow demo-readiness suite were the strongest performer at 12/12 PASS on 2026-05-16 — but the EPIC body and review files have drifted heavily relative to the shipped code (see Drift summary).

Each step names (a) the user story that owns the requirement, (b) the primary code path that runs it, (c) the closest automated test that exercises it, and (d) pre/post-conditions and notes. Where the spec contains no matching catalog story, the step is tagged [SME-PROPOSED] as a NEW story for the SME's intake — these are the gaps the RTM scale-out is designed to surface.

Personas​

PersonaRoleSource
Night AuditorACCOUNTS / MANAGER — operates the dashboard manual-trigger UI when the K8s CronJob is disabled OR a property opts into manual runsEP-005C body §6 (US-005C-01) + hmp-frontdesk-service/src/night-audit/night-audit.controller.ts @Roles('SUPER_ADMIN','ADMIN','MANAGER','ACCOUNTS')
Front-desk ManagerMANAGER — approves rollback within the 2-hour window (per EPIC body §7); receives Day-End Report (DER) at 06:00 ISTEP-005C body §7 + D-006
SUPER_ADMINKeyNova platform operator — can force-rollback outside the 2-hour window; drives chain audit from EP-018 dashboard once shippedEP-005C body §6 (US-005C-06) + D-006
K8s CronJob (service account)Production trigger — the cluster's hmp-night-audit CronJob runs at 20:30 UTC firing POST /api/night-audit/chain with INTERNAL_SERVICE_TOKENhmp-infrastructure/kubernetes/jobs/night-audit-cronjob.yaml (AP-T09-005)

Entry points​

EntryURLBacked by
K8s CronJob (production)n/a — POST /api/night-audit/chain (cluster-internal)kubernetes/jobs/night-audit-cronjob.yaml; calls hmp-frontdesk-service:3005 with INTERNAL_SERVICE_TOKEN
Manual trigger (dashboard)https://dashboard.<env>.keynova.ai/night-audithmp-web-dashboard/src/app/(dashboard)/night-audit/keynova.tsx (stepper: pre-checks → progress → result)
Manual API callPOST /api/night-audit/:propertyId/start[?dryRun=true]hmp-frontdesk-service/src/night-audit/night-audit.controller.ts:252-273
In-process cron (dev/QA fallback)n/a — fires hourly via NestJS @Cron(EVERY_HOUR)hmp-frontdesk-service/src/night-audit/night-audit-scheduler.service.ts; disabled in K8s mode via NIGHT_AUDIT_CRON_DISABLED=true
RollbackPOST /api/night-audit/:id/rollback (MANAGER+)night-audit.controller.ts:339-357
Resume (failed audit)POST /api/night-audit/:id/resume (MANAGER+)night-audit.controller.ts:320-337
DER exportGET /api/night-audit/record/:id/summary?format=pdf|csvnight-audit.controller.ts:120-214 (AP-T09-003)

The 12-step journey​

Step 1: Pre-checks — open shifts? unposted charges? pending guests?​

  • Story: US-005C-01 — Pre-Audit Validation Report (Built, P0, 5 pts).
  • Code: hmp-frontdesk-service/src/night-audit/night-audit.controller.ts:300-318 — GET /night-audit/:propertyId/pre-audit-report returns 4 standard counters (openFoliosWithBalance, confirmedArrivals, pendingDepartures, roomsInDirtyStatus) + canProceed boolean + warnings[]. GET /night-audit/:propertyId/validate returns the same warnings[] structure without the counters. Dashboard surface: hmp-web-dashboard/src/components/modules/night-audit/pre-checks-step.tsx (Pre-Audit Checklist).
  • Test: hmp-frontdesk-service/src/night-audit/night-audit.service.spec.ts :: getPreAuditReport + validatePreAudit coverage (part of the 1,166-line suite). validate-pre-audit-pos.spec.ts (170 lines) — POS open-KOT detection. Flow: flow-9-night-audit.spec.ts :: S1 "Pre-audit report returns 4 counters" — PASS (12/12). Real-backend smoke: smoke-night-audit.real.spec.ts.
  • Pre-conditions: Property exists; tenant ACTIVE; reservation/folio/housekeeping schemas reachable (cross-service reads via @hmp/service-clients).
  • Post-conditions: Warnings array surfaces operational issues. canProceed=false ONLY if blocking warnings present (today: any open RUNNING audit for the property). UI renders each warning with an action button per pre-checks-step.tsx.
  • Notes: [SME-PROPOSED] per EP-005C-rtm.md D-001-area: the report uses 4 counter keys (openFoliosWithBalance/confirmedArrivals/pendingDepartures/roomsInDirtyStatus) which don't 1:1 match the EPIC body's named counters (totalOccupiedRooms/totalChargesAmount are NOT exposed). Pre-audit validation warns but does not block on open POS KOTs (real-world: cashier reconciles next morning) — SME-debated. The cashier-shift open detection does NOT distinguish Morning/Evening shifts (should block) from Night shift (mid-audit, must NOT block) — drift D-008 from EP-005C-rtm.

Step 2: CronJob trigger (K8s, 02:00 IST per schedule)​

  • Story: US-005C-07 — Scheduled Audit (Auto-Trigger) (Built, P1, 5 pts). EPIC body's "Partial" status is stale — production trigger is fully shipped via AP-T09-005.
  • Code: hmp-infrastructure/kubernetes/jobs/night-audit-cronjob.yaml — schedule: "30 20 * * *" (20:30 UTC = 02:00 IST IST), concurrencyPolicy: Forbid, activeDeadlineSeconds: 7200, backoffLimit: 2, successfulJobsHistoryLimit: 3. Pod runs curlimages/curl:8.6.0 and fires POST http://hmp-frontdesk-service.hmp-apps.svc.cluster.local:3005/api/night-audit/chain with Authorization: Bearer ${INTERNAL_SERVICE_TOKEN} (B2 GitOps-managed Secret internal-service-token/token). In-process fallback (dev/QA, when NIGHT_AUDIT_CRON_DISABLED=false or unset): hmp-frontdesk-service/src/night-audit/night-audit-scheduler.service.ts @Cron(EVERY_HOUR) walks every property and starts an audit when local hour matches Property.settings.nightAuditHour (default 2).
  • Test: night-audit-scheduler.service.spec.ts (60 lines) — Cron tick, per-property hour matching, distributed-lock acquisition. CronJob itself is not flow-testable (cron schedule). Manifest validated via kubeconform in Plan 1 T26.
  • Pre-conditions: K8s cluster up; internal-service-token Secret materialized by ESO from keynova/<env>/internal-service-token AWS Secrets Manager (B2); frontdesk-service Deployment healthy; NIGHT_AUDIT_CRON_DISABLED=true set on the frontdesk-service env (otherwise BOTH triggers race and the in-process scheduler reduplicates).
  • Post-conditions: One audit Job per night; sequential per-property processing inside the chain orchestrator (Step 4); successfulJobsHistoryLimit: 3 keeps 3 Job objects for post-mortem inspection.
  • Notes: Indian hospitality reality — 02:00 IST is the standard window: front-desk traffic minimum, F&B closed, kitchen wound down. The K8s CronJob fires at ONE fixed UTC tick globally and iterates ALL properties via the chain endpoint — Property.settings.nightAuditHour per-property override is honoured only by the in-process scheduler (dev/QA), NOT the K8s CronJob. [SME-PROPOSED] AC for documentation: "When NIGHT_AUDIT_CRON_DISABLED=true (production K8s mode), Property.settings.nightAuditHour is ignored; documented limitation — all India-region tenants audit at 02:00 IST."

Step 3: Dry-run option (validate without side effects)​

  • Story: US-005-004 / US-005-07 — Night Audit Automation (Built, P0, 13 pts). The ?dryRun=true branch is AP-T09-001 (rollback expansion era).
  • Code: hmp-frontdesk-service/src/night-audit/night-audit.controller.ts:252-273 — POST /night-audit/:propertyId/start?dryRun=true. Returns structured {dryRun:true, propertyId, wouldProceed:boolean, warnings[]} without acquiring the lock, posting charges, or advancing the business date. @Roles('SUPER_ADMIN','ADMIN','MANAGER','ACCOUNTS'). @Audit({tableName:'night_audit_logs'}) still logs the dry-run attempt for traceability.
  • Test: night-audit.service.spec.ts :: dryRun branch covered in the main suite. Flow: flow-9-night-audit.spec.ts :: S2 + S3 use ?dryRun=true exclusively to avoid side effects on every CI run — PASS (S2 + S3 both green at 12/12).
  • Pre-conditions: ACCOUNTS+ role; property exists.
  • Post-conditions: Pre-audit data returned + dry-run NightAuditLog row (status DRY_RUN or equivalent metadata flag depending on implementation) for traceability. Idempotent — repeated dry-runs are safe.
  • Notes: This branch is what makes the production CronJob safe to test without leaving side effects on the canary property. [SME-PROPOSED] AC: "?dryRun=true MUST NOT acquire the distributed lock; concurrent dry-runs are explicitly permitted (used by flow-9 S6 to probe idempotency)." Dry-run shape is also what the dashboard "Preview" button on pre-checks-step.tsx calls before the user clicks "Run Audit".

Step 4: Lock acquired (distributed lock per property)​

  • Story: US-005-004 / US-005-07 — Night Audit Automation (concurrency guard).
  • Code: hmp-frontdesk-service/src/night-audit/night-audit.service.ts::acquireAuditLock + releaseAuditLock. Lock key pattern: cron:night-audit:${propertyId} (per scheduler) and an analogous in-service lock for the manual-trigger path. Backed by @hmp/cache Redis. Concurrent start while a RUNNING NightAuditLog row exists for the same (propertyId, businessDate) → ConflictException (HTTP 409). onModuleInit runs GSA-133 stale-RUNNING recovery (catches audit logs orphaned by a pod restart mid-run).
  • Test: night-audit.service.spec.ts :: concurrency-lock specs + GSA-133 stale-RUNNING recovery. Flow: flow-9-night-audit.spec.ts :: S6 "duplicate-start guard" — PASS (12/12; full-run path gated by FLOW9_FULL_AUDIT=true env). The chain orchestrator runs properties sequentially to avoid DB-lock contention (per CLAUDE.md Gotcha).
  • Pre-conditions: Redis reachable; no other RUNNING audit for the property.
  • Post-conditions: NightAuditLog row created with status='RUNNING', startedAt=now(), tenantId resolved + persisted (GSA-014/015). Lock TTL 30 min (matches withLock default for cron-triggered work).
  • Notes: Indian-chain reality (per Step 11 / US-005C-06) — a chain of 10 properties audited sequentially × ~15 min each = 2.5 hr total, JUST exceeds the CronJob's 7200s deadline. The activeDeadlineSeconds fail-safe is sized for 8-property tenants (drift D-NEW-AD: [SME-PROPOSED] "Calibrate activeDeadlineSeconds to tenantPropertyCount × 20 minutes + 30 min buffer per overlay"). Lock contention on a single Redis is the canonical reason the chain runs sequentially, not in parallel — large tenants will need either bigger Redis or chain-level concurrency control.

Step 5: Post outstanding room charges for in-house guests​

  • Story: US-005-004 / US-005-07 — Night Audit Automation (core charge-posting step).
  • Code: night-audit.service.ts::processNightAudit step 3 (room-charge posting) → for each in-house reservation with an OPEN folio, posts transactionCode='ROOM' charge via hmp-frontdesk-service/src/folios/folios.service.ts::postCharge. GST split persisted at posting time (W2.A.1 + AP-T07-001/004 deriveGstSplit(customerState, propertyState) decides CGST+SGST vs IGST). Slab pinned by calculateGSTForRoom() from @hmp/constants (≤1000 INR = 12%, ≤7500 = 18%, >7500 = 28%). SAC code 996311 annotation is the single unverified gap from the 2026-03-07 review file (D-002 from EP-005C-rtm.md).
  • Test: night-audit.service.spec.ts :: full-pipeline coverage incl. charge posting. night-audit-gst.spec.ts :: GST slab pinning. Flow: flow-9-night-audit.spec.ts :: S3 "Post room charges" — PASS (12/12; verified via dryRun wouldProceed + full-run path under FLOW9_FULL_AUDIT=true).
  • Pre-conditions: Step 4 lock held; in-house reservation list populated via cross-service read; tax config + GST slab resolvable per room type.
  • Post-conditions: One FolioTransaction row per in-house folio with the day's room rate + GST split; Folio.balance recomputed; FOLIO_POSTED event published downstream (FU-55 dual-emit emits BOTH canonical + deprecated FOLIO_CHARGE_POSTED for back-compat).
  • Notes: Indian-hospitality cut-off — GST liability accrues at the moment of charge posting, not at check-out. A reservation that checks out on Day-N+1 must already have Day-N's room charge + GST persisted so the e-invoice batch (Step 9 cousin) sees a complete liability picture. The night audit IS the GST cut-off — without it, finance can't reconcile yesterday's revenue. [SME-PROPOSED] AC: "Room charge line items SHALL carry sacCode='996311' (SAC for Hotel & Lodging) on the GST e-invoice payload — finishes the AP-T08-003 IRN compliance work." Closes the single residual gap from the 2026-03-07 US-005-004 review (D-002).

Step 6: Post late check-out + no-show charges​

  • Story: US-005C-02 — No-Show Processing (catalog says Not Started, code says Built, P0, 8 pts — drift D-004 from EP-005C-rtm.md). Late-check-out / overstay handling lives inside the same audit step pipeline.
  • Code: night-audit.service.ts::processNightAudit step 2 — find CONFIRMED reservations with arrivalDate < today AND no checkedInAt, mark as NO_SHOW, post first-night rate + GST as transactionCode='NO_SHOW'. Overstay (guest still in-house past departureDate) posts as transactionCode='OVERSTAY_NIGHT'. Per src/night-audit/CLAUDE.md Gotcha 3: "Only the first night's rate is charged as a no-show fee, not the full reservation amount."
  • Test: night-audit.service.spec.ts :: no-show flow specs (mark NO_SHOW, post first-night charge, KPI counter noShowsProcessed); AP-T09-001 rollback pinning test confirms NO_SHOW + OVERSTAY_NIGHT + ROOM are all voided together. Flow: flow-9-night-audit.spec.ts :: S5 (full-run path under FLOW9_FULL_AUDIT=true) surfaces noShowsProcessed count on the audit history record.
  • Pre-conditions: Step 4 lock held; CONFIRMED arrivals list resolvable; cancellation policy populated on reservation (today: ignored — see Notes).
  • Post-conditions: Reservation.status='NO_SHOW'; FolioTransaction.transactionCode='NO_SHOW'/'OVERSTAY_NIGHT' rows posted with GST split; KPI counters incremented; reservation.no_show event published for EP-014 notification fan-out.
  • Notes: [SME-PROPOSED] per D-005 from EP-005C-rtm.md — code charges first-night rate unconditionally; does NOT consult Reservation.cancellationPolicy.noShowCharge. EPIC Gherkin explicitly demands the Flexible-policy 0%-charge branch. A "0% no-show" tariff cannot be honoured today. AC proposed: "No-show charge amount SHALL be computed as policy.noShowCharge × firstNightRate when noShowCharge is a percentage 0..100, OR equal noShowChargeFlat when the policy uses a flat-amount mode. A null noShowCharge defaults to 100%." Plus: "The no-show charge SHALL inherit the same GST split (CGST+SGST vs IGST) as a regular room charge." Consumer-Protection (E-Commerce) Rules 2020: no-show charge MUST be disclosed at booking — today's UI does, but the audit step doesn't snapshot policy version → recoverable from Reservation.cancellationPolicySnapshot if it exists.

Step 7: Advance business date​

  • Story: US-005-004 / US-005-07 — Night Audit Automation (date-advance step). Aligned with AP-T09-004 (Property.currentBusinessDate field).
  • Code: night-audit.service.ts::_runNightAudit advances Property.currentBusinessDate += 1 day inside the same Prisma transaction as the charge posting (atomicity guarantee). getPropertyLocalMidnight(timezone) returns the property's local midnight for date arithmetic — handles IST-only properties today but is timezone-safe for the multi-region future. POS business-day cutover via pos-business-day.client.ts is fired separately (cross-service call to hmp-pos-service).
  • Test: night-audit.service.spec.ts :: date-advance specs covered in main suite. Flow: flow-9-night-audit.spec.ts :: S4 "Advance business date" — PASS (full-run path under FLOW9_FULL_AUDIT=true); asserts afterDate - beforeDate === 86_400_000 ms (exactly 1 day).
  • Pre-conditions: Steps 5 + 6 completed in the same transaction (atomicity); Property.currentBusinessDate populated (AP-T09-004 schema field).
  • Post-conditions: Property.currentBusinessDate = previous + 1 day. POS-service business day rolled forward via pos-business-day.client.ts. Channel-service rate push, OTA sync, and analytics MVs all now see "today" = audit's next day.
  • Notes: This is the single most irreversible action in the platform. Once currentBusinessDate advances, every downstream consumer (POS day-close, channel rate push, analytics revenue MV) treats yesterday as closed. Rollback (Step 12) reverses currentBusinessDate but cannot un-push rates to OTAs. Indian-hospitality reality: 02:00 IST is the canonical cut-off precisely because the rate push to OTAs happens immediately after — if you delay until 04:00 IST, the early-morning rate-shopping booking window is missed.

Step 8: Generate Day-End Report (DER)​

  • Story: US-005-22 — Night Audit Report & Logs (Built, P0, 5 pts). PDF + CSV both shipped via AP-T09-003 (PDF in 2026-04-29; CSV W2.D.7 in 2026-05-23).
  • Code: night-audit.controller.ts:120-214 — GET /night-audit/record/:id (record for export), GET /night-audit/record/:id/pdf, GET /night-audit/record/:id/summary?format=pdf|csv. Generator: night-audit-pdf.service.ts (248 lines) — generatePdf(record) and generateCsv(record) (RFC-4180 Metric,Value table with KPIs + metadata + one Discrepancy N row per error). History: GET /night-audit/:propertyId/history lists past runs with KPIs. Dashboard surface: hmp-web-dashboard/src/components/modules/night-audit/audit-result-step.tsx (revenue summary, room stats, exception log).
  • Test: night-audit-pdf.service.spec.ts (164 lines) — Header, KPI rows, metadata rows, discrepancy rows, RFC-4180 escaping (commas/quotes/CR/LF), Decimal-object handling. night-audit.controller.spec.ts :: default→PDF, ?format=csv, uppercase CSV, unsupported→400, role gates.
  • Pre-conditions: Step 7 completed; NightAuditLog row in COMPLETED (or COMPLETED_WITH_ERRORS) status; KPIs computed (occupancy %, ADR, RevPAR, no-show count).
  • Post-conditions: PDF + CSV downloadable from controller; KPIs visible in dashboard history list; chain audit aggregate available via GET /chain/status/:trackingId (US-005C-06).
  • Notes: Indian-hospitality reality — the DER (also called "Manager's Flash") lands in the GM's inbox at 06:00–07:00 IST; finance imports the CSV into Tally / Zoho Books. Both formats are non-negotiable. [SME-PROPOSED] per D-003 from EP-005C-rtm.md: "Night audit summary CSV/PDF SHALL include a per-source revenue breakdown table (DIRECT / OTA_BOOKING_COM / OTA_AGODA / OTA_MAKEMYTRIP / OTA_GOIBIBO / WALKIN / CORPORATE) sourced from Reservation.source on charges posted that audit day. Numeric rows: revenue, room nights, ADR per source." Plus a per-room-type breakdown (room-type code, rooms occupied, revenue, ADR, RevPAR). Today finance must cross-reference analytics-service revenue_by_source MV — friction.

Step 9: Reconcile folios (sum check)​

  • Story: [SME-PROPOSED] new story — US-005C-NEW-RECONCILE "Audit-time folio reconciliation invariant check". The current code computes per-folio totals during posting but does not run a closing balance-sheet assertion. EPIC body §4 implies this ("reconcile folios — sum check") but no code path enforces an invariant.
  • Code: Implicit in night-audit.service.ts::processNightAudit — Folio.balance is recomputed on each postCharge call (via folios.service.ts); the audit step iterates in-house folios and posts. There is no explicit cross-folio sum check that Σ(folio.charges) - Σ(folio.payments) = Σ(folio.balance) at audit close. KPI computation (revenue total, ADR, occupancy %) does an aggregate query but it is for reporting, not invariant assertion. The closest existing check is the AP-T09-001 rollback pinning test which verifies post-rollback folio totals.
  • Test: night-audit.service.spec.ts :: KPI aggregation specs. No dedicated reconciliation-invariant spec.
  • Pre-conditions: Steps 5 + 6 completed; all charges + payments visible to the audit transaction.
  • Post-conditions: Today: KPIs reported, exceptions logged. Proposed: invariant violation surfaces as a COMPLETED_WITH_ERRORS audit status with a RECONCILIATION_FAILED discrepancy row.
  • Notes: Indian-hospitality reality — a finance manager cross-checks the DER against the dashboard's "open folio balance" total every morning. If they don't tally, the audit silently shipped a bad number. [SME-PROPOSED] AC: "After Step 5 + 6 and before Step 7, the audit SHALL compute expectedBalance = Σ(folio.charges) - Σ(folio.payments) and assert equality with Σ(folio.balance) across all in-house folios. Mismatches SHALL log one RECONCILIATION_FAILED discrepancy per folio and flip audit status to COMPLETED_WITH_ERRORS. Audit SHALL still complete (date-advance + event publish) so downstream systems don't block, but the DER prominently flags the variance." This is the platform's missing integrity gate at the GST cut-off point.

Step 10: Publish night.audit.completed event (Kafka / Redis Streams)​

  • Story: US-005-004 / US-005-07 — Night Audit Automation (event-publish step; AP-T09-002 transactional outbox).
  • Code: hmp-frontdesk-service/src/night-audit/night-audit-outbox.service.ts — W2.D.5 transactional outbox: a NightAuditOutbox row is INSERTed in the SAME Prisma transaction as the NightAuditLog status flip to COMPLETED. A 30-second background cron relay drains PENDING rows to the message bus (Redis Streams via @hmp/messaging). DLQ replay via POST /night-audit/outbox/:id/replay. Event shape: {auditLogId, propertyId, tenantId, businessDate, completedAt, kpis: {occupancy, adr, revpar, noShowsProcessed, roomsCharged, totalRevenue}}. Consumers: hmp-analytics-service/src/dispatcher/event-dispatcher.service.ts (NightAuditConsumer) fans into 5 MV consumers.
  • Test: night-audit-outbox.service.spec.ts :: enqueue + relay + DEAD_LETTER. outbox-replay-endpoint.spec.ts :: DLQ replay controller. Analytics-side: NightAuditConsumer specs in hmp-analytics-service.
  • Pre-conditions: Step 7 + Step 8 completed; transaction successfully committed.
  • Post-conditions: Outbox row PENDING → relay drains → message bus emits → analytics MVs ingest. A Kafka outage during audit completion no longer loses the event (W2.D.5 closes the prior fire-and-forget gap).
  • Notes: The outbox pattern was added because the original fire-and-forget publish silently dropped events on transient broker failures — P0 for analytics MV ingestion. [SME-PROPOSED] per D-006 from EP-005C-rtm.md: "Successful rollback SHALL publish night.audit.rolled_back event via the same outbox pattern" — today rollback is silent to analytics, so MV numbers go stale until re-run.

Step 11: Release lock + log result (and chain advance for multi-property tenants)​

  • Story: US-005C-06 — Chain Audit Orchestration (catalog says Not Started, code says Built, P2, 13 pts — drift carries forward from D-002 family). Per-property lock release is internal to US-005-004.
  • Code: night-audit.service.ts::releaseAuditLock (or equivalent finally block) on success or failure. NightAuditLog row → COMPLETED / COMPLETED_WITH_ERRORS / FAILED with completedAt + KPIs persisted. Chain orchestrator: chain-audit-orchestrator.service.ts (362 lines) — startChainAuditAsync(tenantId, userId) + getChainAuditStatus(trackingId); aggregates per-property results; processes properties sequentially to avoid DB-lock contention. night-audit.controller.ts:216-248 — POST /night-audit/chain (returns 202 + trackingId) + GET /night-audit/chain/status/:trackingId.
  • Test: chain-audit-orchestrator.service.spec.ts (254 lines) — sequential audit, aggregation, blocked-property handling, chain summary. No flow-test for chain (per-property only via flow-9).
  • Pre-conditions: Step 10 outbox enqueue committed.
  • Post-conditions: Lock released; NightAuditLog terminal status set; next property in the chain begins (if chain run). Chain trackingId tracks per-property success/failure/blocked counts.
  • Notes: [SME-PROPOSED] per US-005C-06 SME notes: "Chain audit SHALL publish a single night.audit.chain.completed event after the last property finishes, with {tenantId, trackingId, totalProperties, succeeded, failed, blocked, totalRevenue, completedAt}. EP-018 chain dashboard consumes this for the cross-property roll-up." Today each property emits its own night.audit.completed; the aggregate per-tenant signal is missing. Indian-chain reality: most Tier-2 Indian chains run 3–10 properties; sequential audit (1 property × 15 min × 10) = 2.5 hr fits inside the 02:00–05:00 IST window but JUST exceeds the CronJob's activeDeadlineSeconds: 7200 (see Step 4 SME note).

Step 12: Rollback path if any step fails (2-hour window)​

  • Story: US-005C-03 — Night Audit Rollback (catalog says Partial, code says Built for the 3 transaction codes, P1, 8 pts). 2-hour window is NOT implemented (drift D-006 from EP-005C-rtm.md).
  • Code: night-audit.controller.ts:339-357 — POST /night-audit/:id/rollback; @Roles('SUPER_ADMIN','ADMIN','MANAGER') (note: not ACCOUNTS); @Audit({tableName:'night_audit_logs'}). night-audit.service.ts::rollbackNightAudit voids ROOM + OVERSTAY_NIGHT + NO_SHOW transactions in one pass (AP-T09-001 expansion 2026-04-28; before this, only ROOM was voided and overstay/no-show charges were orphaned on folios). Recomputes folio totals, reverts Property.currentBusinessDate, sets NightAuditLog.status='ROLLED_BACK'. Resume (related): night-audit.controller.ts:320-337 POST /night-audit/:id/resume — marks failed audit ROLLED_BACK then re-runs full audit (transaction safety ensures no double-posting). Status gate: only FAILED audits can be resumed.
  • Test: night-audit.service.spec.ts :: "AP-T09-001: rollback voids ROOM, OVERSTAY_NIGHT, and NO_SHOW transactions in the same call" — pinning test. Flow: flow-9-night-audit.spec.ts :: afterAll() attempts rollback to keep test property reusable across runs.
  • Pre-conditions: NightAuditLog row exists; MANAGER+ role; today: NO time-window check (audit from 3 days ago can be rolled back today).
  • Post-conditions: All ROOM/OVERSTAY_NIGHT/NO_SHOW charges voided; folio totals recomputed; Property.currentBusinessDate reverted; NightAuditLog status = ROLLED_BACK.
  • Notes: [SME-PROPOSED] per D-006 from EP-005C-rtm.md: "Rollback SHALL be rejected with code=NIGHT_AUDIT_ROLLBACK_WINDOW_EXPIRED (HTTP 409) when Date.now() - completedAt > 2 hours. SUPER_ADMIN MAY override via explicit ?force=true query param + reason; override emits night.audit.rollback.forced audit-log entry." Plus the missing night.audit.rolled_back event (Step 10 note). Indian-hospitality reality: Day-N+1 has already advanced for downstream systems (POS day-close, channel rate push to OTAs, OTA sync). Rolling back hours later = cascading inconsistency — channel-service has already pushed today's rates to MakeMyTrip / Booking.com / Agoda, and there's no automatic re-push on rollback. The 2-hour window protects against this footgun.

Failure modes worth testing​

#ScenarioExpected behaviourCoverage todayOwning story
F1Two CronJob ticks overlap (previous run hung > 24h, next 02:00 IST fires)concurrencyPolicy: Forbid skips the second tick; ops alertK8s manifest validated; no integration testUS-005C-07
F2Distributed lock contention — manual MANAGER trigger fires while in-process scheduler is mid-auditSecond start returns 409 with ConflictException; idempotency holdsnight-audit.service.spec.ts concurrency-lock specs; flow-9-night-audit.spec.ts S6 (full-run path under FLOW9_FULL_AUDIT=true) — PASSUS-005-004
F3Partial post failure — Folio row locked by frontdesk transaction mid-auditAudit step fails inside Prisma transaction; ALL partial work rolls back; NightAuditLog row → FAILED; lock releasednight-audit.service.spec.ts transaction-rollback specs; AP-T09-001 pinning testUS-005-004
F4Cron pod OOM (large chain, 50+ properties, memory spike during PDF gen)Pod OOMKilled; restartPolicy: OnFailure + backoffLimit: 2 allow 2 retries; if all fail → Job Failed; successfulJobsHistoryLimit: 3 keeps prior runs for debugNot tested; resource limits in CronJob requests: 32Mi / limits: 64Mi (curl pod only — frontdesk-service handles the work)[SME-PROPOSED] US-005C-NEW-OBSERVABILITY
F5K8s activeDeadlineSeconds: 7200 exceeded (chain audit on 10+ properties × 15 min)Job marked Failed at 2hr mark mid-property; that property's audit may be in RUNNING state → GSA-133 stale-recovery on next pod bootnight-audit.service.spec.ts GSA-133 stale-RUNNING recovery specUS-005C-07 (calibration AC proposed in Step 4)
F6Downstream Kafka consumer crash (analytics NightAuditConsumer dies after outbox relay)Audit completes successfully (outbox row marked PUBLISHED); consumer-side replay required — outbox/:id/replay endpoint reprocessesnight-audit-outbox.service.spec.ts + outbox-replay-endpoint.spec.tsUS-005-004 (AP-T09-002)
F7Business-date already advanced (idempotency check) — chain orchestrator retries a propertyDuplicate-date guard catches COMPLETED row for (propertyId, businessDate); second call is no-opnight-audit.service.spec.ts idempotency spec; per night-audit-cronjob.yaml header comment: "re-running for an already-audited date is a no-op"US-005-004
F8Rollback while another property is mid-audit (chain in flight, MANAGER rolls back property 3 of 10)Today: rollback succeeds against the completed property without affecting in-flight chain; currentBusinessDate reverts ONLY for the rolled-back property; chain orchestrator continues — but EP-018 chain admin UI cannot see the consistency breakchain-audit-orchestrator.service.spec.ts (chain orchestration); no cross-property rollback test[SME-PROPOSED] US-005C-03-followup
F9INTERNAL_SERVICE_TOKEN Secret stale after JWT keypair rotationCron pod's curl gets 401 from frontdesk-service; Job fails; pod retries 2× per backoffLimitB2 (ESO + Reloader) auto-re-mints on JWT rotation; tested in production 2026-05-24 (auto-rotation fired clean per session memory)Cross-cuts US-005C-07 + infra B2
F10NIGHT_AUDIT_CRON_DISABLED=true not set in K8s mode — both schedulers fireIn-process scheduler + K8s CronJob both attempt — distributed lock catches the second; but unnecessary load on RedisDocumented in EP-005C-rtm SME notes; no integration testUS-005C-07

Day-2 follow-ups (NOT in BF-006 scope)​

Items that surface during the night-audit journey but belong in adjacent flows or post-launch hardening:

  • E-invoice batch at day-close — once GST charges are persisted via Step 5, the IRN batch job runs on Day-N+1 morning per AP-T08-003 (frontdesk-service EInvoiceModule). Mock allowed in dev via EINVOICE_ALLOW_MOCK_IN_PROD=true (W0.1.1 boot guard). Belongs in BF-005 Check-out + e-invoice.
  • OTA rate re-push after rollback — the missing night.audit.rolled_back event (Step 12 note) means channel-service does not auto-revert the rate push. Operator must manually fire a channel-sync from the dashboard. Belongs in BF-007 OTA Channel Sync.
  • EP-018 chain admin UI — chain orchestrator backend shipped; the tenant-level dashboard to drive POST /night-audit/chain and render GET /chain/status/:trackingId is not started (drift carries forward from US-005C-06).
  • Property-level nightAuditHour for K8s mode — today only honoured by in-process scheduler. If a multi-region chain ships, the K8s CronJob needs a per-property fan-out (or a per-region CronJob) — see Step 2 SME note.
  • Cashier-shift close integration with audit-blocking — US-005C-05 backend is built but the night-audit pre-check that distinguishes Morning/Evening (block) from Night (allow) is not (drift D-008 from EP-005C-rtm).
  • Reconciliation-invariant assertion — the proposed Step 9 invariant check is the platform's missing integrity gate at the GST cut-off — see [SME-PROPOSED] US-005C-NEW-RECONCILE.
  • Night-shift cashier handover UX — Indian-hospitality reality is that the night auditor IS the night-shift cashier; the 02:00 IST audit happens mid-shift. The dashboard does not surface a "handover" affordance to mark "I am the auditor running this" for accountability — [SME-PROPOSED] US-005C-NEW-HANDOVER.
  • Per-source revenue breakdown in DER — D-003 from EP-005C-rtm; finance currently cross-references analytics-service revenue_by_source MV.

Drift summary​

Drift IDs are inherited from ../reconciliation/EP-005C-rtm.md and re-surfaced here through the journey lens. (P0 = blocks property go-live; P1 = ships next sprint; P2 = backlog.)

IDTypeSeverityDescriptionSurfaced by step
D-001Story-numbering-driftP0Three numbering systems coexist for the same surface: EPIC body uses US-005-07; catalog uses US-005-004; review folder uses US-005-004-review.md. A reviewer following the EPIC link won't find the review file; a reviewer reading the review file won't find the EPIC sub-story. Adopt US-005-004 as canonical with US-005C-01..07 as the sub-epic story IDs.Steps 3, 5, 7, 10, 12 (any step that cites US-005-004 / US-005-07)
D-002Stale-review-vs-shipped-codeP0reviews/EP-005/US-005-004-review.md (2026-03-07) marks 6 ACs NOT_IMPL: SAC 996311, business-date advance, Kafka event, no-show charge, IGST, rollback. 5 of 6 have shipped. Only SAC 996311 annotation on the room-charge line item remains unverified.Step 5 (SAC annotation gap), Steps 6/7/10/12 (already shipped)
D-003Story-incomplete + India-hospitality-criticalP1DER CSV/PDF does NOT include per-source revenue breakdown (DIRECT / OTA-by-channel / WALKIN / CORPORATE). Single most-used split on the Manager's Flash; finance cross-references analytics-service MV today.Step 8
D-004Catalog-vs-codeP0EPIC body §6 marks US-005C-02 No-Show Processing as Not Started; code has shipped + is tested + verified by flow-9 S5. New engineer would re-author closed work. Same drift family as D-002.Step 6
D-005Story-incomplete + India-hospitality-criticalP1No-show charge does NOT consult CancellationPolicy.noShowCharge. EPIC Gherkin explicitly demands Flexible-policy 0%-charge branch — code charges first-night unconditionally. "0% no-show" tariff cannot be honoured today.Step 6
D-006Story-incomplete + missing-guardP1EPIC body specifies 2-hour rollback window ("Rollback window expired — contact Finance Manager") — not implemented. Audit from 3 days ago can be rolled back today with no operator gate. Plus missing night.audit.rolled_back event → downstream analytics MVs go stale.Step 12 (+ Step 10 for event)
D-007Story-vs-impl-design-mismatchP2EPIC body specifies step-aware resume via Redis-persisted step state; implementation is full re-run under transaction safety. Simplification is defensible but EPIC body has not been updated.(Related — not in 12-step journey; touched by Step 4 + Step 12 commentary)
D-008Story-incomplete + India-hospitality-criticalP1Cashier-shift open detection does NOT distinguish Morning/Evening shifts (should block audit) from Night shift (mid-audit, must NOT block). A naive "any open shift blocks" check would prevent 02:00 IST audit from ever running.Step 1
D-009[SME-PROPOSED] NEW — integrity gateP0No closing folio-reconciliation invariant assertion at audit close. Finance cross-checks DER against dashboard open-folio balance manually every morning. If they don't tally, audit silently shipped a bad number → GST cut-off corrupted.Step 9
D-010[SME-PROPOSED] NEW — Indian-hospitality-criticalP1Step 5 room-charge line items must carry sacCode='996311' (SAC for Hotel & Lodging) on the GST e-invoice payload. Closes the single residual gap from the 2026-03-07 US-005-004 review (D-002 sub-gap).Step 5
D-011[SME-PROPOSED] NEW — chain scalabilityP1K8s activeDeadlineSeconds: 7200 (2 hr) is sized for 8-property tenants. Larger chains (10+ properties × 15 min each) WILL hit the deadline; that property's audit ends in RUNNING state requiring GSA-133 stale-recovery. Calibrate per-overlay.Steps 4 + 11
D-012[SME-PROPOSED] NEW — chain observabilityP1Chain audit does not emit aggregated night.audit.chain.completed event for EP-018 chain dashboard. Per-property events emit but no per-tenant roll-up signal.Step 11

Cross-EPIC narrative summary​

StepSpansWhy cross-EPIC
1EP-005C ↔ EP-005B (POS) ↔ EP-006 (HK)Pre-audit reads open KOTs (POS) + dirty room status (HK) + open folios (frontdesk) via @hmp/service-clients.
2EP-005C ↔ InfrastructureK8s CronJob in hmp-infrastructure; in-process scheduler in hmp-frontdesk-service. The NIGHT_AUDIT_CRON_DISABLED flag is the seam.
3EP-005CDry-run is internal to frontdesk-service.
4EP-005C ↔ Shared Redis (@hmp/cache)Distributed lock seam.
5EP-005C ↔ EP-005A (Folios) ↔ EP-002 (Tax config)Charge posting calls folios.service.ts::postCharge with GST split derived from property tax config + AP-T07-004 deriveGstSplit.
6EP-005C ↔ EP-003 (Reservations) ↔ EP-014 (Notifications)No-show flips Reservation.status; publishes reservation.no_show event for EP-014 email fan-out.
7EP-005C ↔ EP-002 (Property) ↔ EP-005B (POS) ↔ EP-009 (Channel)Property.currentBusinessDate advance triggers POS day-close + channel rate push. The most-cross-cutting step.
8EP-005C ↔ EP-012 (Reporting)DER PDF/CSV generation. Finance consumes via CSV import to Tally/Zoho.
9EP-005C ↔ EP-005A (Folios)Proposed cross-folio reconciliation invariant.
10EP-005C ↔ EP-012B (Analytics)Outbox → message bus → NightAuditConsumer → 5 MVs. AP-T09-002 closure.
11EP-005C ↔ EP-018 (Chain admin)Chain orchestrator backend shipped; EP-018 dashboard not started.
12EP-005C ↔ EP-005A (Folios) ↔ EP-002 (Property) ↔ EP-009 (Channel)Rollback voids folio transactions + reverts business date — but does NOT auto-revert OTA rate push (operator must manually fire channel-sync).

The K8s-CronJob ↔ in-process-scheduler seam (Step 2) and the charge-posting-then-business-date-advance atomicity boundary (Steps 5 + 7) are the primary BF-006 findings the SME must act on. The GST cut-off semantics of Step 5 (timing of liability accrual) and the missing reconciliation invariant of Step 9 are the highest-leverage Indian-hospitality SME calls on this journey.