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.mdand 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
| Persona | Role | Source |
|---|---|---|
| Night Auditor | ACCOUNTS / MANAGER — operates the dashboard manual-trigger UI when the K8s CronJob is disabled OR a property opts into manual runs | EP-005C body §6 (US-005C-01) + hmp-frontdesk-service/src/night-audit/night-audit.controller.ts @Roles('SUPER_ADMIN','ADMIN','MANAGER','ACCOUNTS') |
| Front-desk Manager | MANAGER — approves rollback within the 2-hour window (per EPIC body §7); receives Day-End Report (DER) at 06:00 IST | EP-005C body §7 + D-006 |
| SUPER_ADMIN | KeyNova platform operator — can force-rollback outside the 2-hour window; drives chain audit from EP-018 dashboard once shipped | EP-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_TOKEN | hmp-infrastructure/kubernetes/jobs/night-audit-cronjob.yaml (AP-T09-005) |
Entry points
| Entry | URL | Backed 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-audit | hmp-web-dashboard/src/app/(dashboard)/night-audit/keynova.tsx (stepper: pre-checks → progress → result) |
| Manual API call | POST /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 |
| Rollback | POST /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 export | GET /api/night-audit/record/:id/summary?format=pdf|csv | night-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-reportreturns 4 standard counters (openFoliosWithBalance,confirmedArrivals,pendingDepartures,roomsInDirtyStatus) +canProceedboolean +warnings[].GET /night-audit/:propertyId/validatereturns the samewarnings[]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+validatePreAuditcoverage (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=falseONLY if blocking warnings present (today: any open RUNNING audit for the property). UI renders each warning with an action button perpre-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/totalChargesAmountare 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 runscurlimages/curl:8.6.0and firesPOST http://hmp-frontdesk-service.hmp-apps.svc.cluster.local:3005/api/night-audit/chainwithAuthorization: Bearer ${INTERNAL_SERVICE_TOKEN}(B2 GitOps-managed Secretinternal-service-token/token). In-process fallback (dev/QA, whenNIGHT_AUDIT_CRON_DISABLED=falseor 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 matchesProperty.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 viakubeconformin Plan 1 T26. - Pre-conditions: K8s cluster up;
internal-service-tokenSecret materialized by ESO fromkeynova/<env>/internal-service-tokenAWS Secrets Manager (B2); frontdesk-service Deployment healthy;NIGHT_AUDIT_CRON_DISABLED=trueset 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: 3keeps 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.nightAuditHourper-property override is honoured only by the in-process scheduler (dev/QA), NOT the K8s CronJob.[SME-PROPOSED]AC for documentation: "WhenNIGHT_AUDIT_CRON_DISABLED=true(production K8s mode),Property.settings.nightAuditHouris 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=truebranch 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=trueexclusively 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_RUNor 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=trueMUST 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 onpre-checks-step.tsxcalls 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/cacheRedis. Concurrent start while aRUNNINGNightAuditLog row exists for the same(propertyId, businessDate)→ConflictException(HTTP 409).onModuleInitruns GSA-133 stale-RUNNINGrecovery (catches audit logs orphaned by a pod restart mid-run). - Test:
night-audit.service.spec.ts:: concurrency-lock specs + GSA-133 stale-RUNNINGrecovery. Flow:flow-9-night-audit.spec.ts:: S6 "duplicate-start guard" — PASS (12/12; full-run path gated byFLOW9_FULL_AUDIT=trueenv). The chain orchestrator runs properties sequentially to avoid DB-lock contention (perCLAUDE.mdGotcha). - Pre-conditions: Redis reachable; no other
RUNNINGaudit for the property. - Post-conditions: NightAuditLog row created with
status='RUNNING',startedAt=now(),tenantIdresolved + persisted (GSA-014/015). Lock TTL 30 min (matcheswithLockdefault 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]"CalibrateactiveDeadlineSecondstotenantPropertyCount × 20 minutes + 30 min bufferper 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::processNightAuditstep 3 (room-charge posting) → for each in-house reservation with an OPEN folio, poststransactionCode='ROOM'charge viahmp-frontdesk-service/src/folios/folios.service.ts::postCharge. GST split persisted at posting time (W2.A.1 + AP-T07-001/004deriveGstSplit(customerState, propertyState)decides CGST+SGST vs IGST). Slab pinned bycalculateGSTForRoom()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 dryRunwouldProceed+ full-run path underFLOW9_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
FolioTransactionrow per in-house folio with the day's room rate + GST split;Folio.balancerecomputed;FOLIO_POSTEDevent published downstream (FU-55 dual-emit emits BOTH canonical + deprecatedFOLIO_CHARGE_POSTEDfor 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 carrysacCode='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::processNightAuditstep 2 — findCONFIRMEDreservations witharrivalDate < todayAND nocheckedInAt, mark asNO_SHOW, post first-night rate + GST astransactionCode='NO_SHOW'. Overstay (guest still in-house pastdepartureDate) posts astransactionCode='OVERSTAY_NIGHT'. Persrc/night-audit/CLAUDE.mdGotcha 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 (markNO_SHOW, post first-night charge, KPI counternoShowsProcessed); 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 underFLOW9_FULL_AUDIT=true) surfacesnoShowsProcessedcount 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_showevent 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 consultReservation.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 aspolicy.noShowCharge × firstNightRatewhennoShowChargeis a percentage 0..100, OR equalnoShowChargeFlatwhen the policy uses a flat-amount mode. A nullnoShowChargedefaults 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 fromReservation.cancellationPolicySnapshotif 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.currentBusinessDatefield). - Code:
night-audit.service.ts::_runNightAuditadvancesProperty.currentBusinessDate += 1 dayinside 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 viapos-business-day.client.tsis fired separately (cross-service call tohmp-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 underFLOW9_FULL_AUDIT=true); assertsafterDate - beforeDate === 86_400_000 ms(exactly 1 day). - Pre-conditions: Steps 5 + 6 completed in the same transaction (atomicity);
Property.currentBusinessDatepopulated (AP-T09-004 schema field). - Post-conditions:
Property.currentBusinessDate= previous + 1 day. POS-service business day rolled forward viapos-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
currentBusinessDateadvances, every downstream consumer (POS day-close, channel rate push, analytics revenue MV) treats yesterday as closed. Rollback (Step 12) reversescurrentBusinessDatebut 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)andgenerateCsv(record)(RFC-4180Metric,Valuetable with KPIs + metadata + oneDiscrepancy Nrow per error). History:GET /night-audit/:propertyId/historylists 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, uppercaseCSV, unsupported→400, role gates. - Pre-conditions: Step 7 completed; NightAuditLog row in
COMPLETED(orCOMPLETED_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 fromReservation.sourceon 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-servicerevenue_by_sourceMV — 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.balanceis recomputed on eachpostChargecall (viafolios.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_ERRORSaudit status with aRECONCILIATION_FAILEDdiscrepancy 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 computeexpectedBalance = Σ(folio.charges) - Σ(folio.payments)and assert equality withΣ(folio.balance)across all in-house folios. Mismatches SHALL log oneRECONCILIATION_FAILEDdiscrepancy per folio and flip audit status toCOMPLETED_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: aNightAuditOutboxrow is INSERTed in the SAME Prisma transaction as the NightAuditLog status flip toCOMPLETED. A 30-second background cron relay drains PENDING rows to the message bus (Redis Streams via@hmp/messaging). DLQ replay viaPOST /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 inhmp-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 publishnight.audit.rolled_backevent 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/FAILEDwithcompletedAt+ 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 singlenight.audit.chain.completedevent 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 ownnight.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'sactiveDeadlineSeconds: 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::rollbackNightAuditvoidsROOM+OVERSTAY_NIGHT+NO_SHOWtransactions 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, revertsProperty.currentBusinessDate, setsNightAuditLog.status='ROLLED_BACK'. Resume (related):night-audit.controller.ts:320-337POST /night-audit/:id/resume— marks failed auditROLLED_BACKthen re-runs full audit (transaction safety ensures no double-posting). Status gate: onlyFAILEDaudits 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.currentBusinessDatereverted; NightAuditLog status =ROLLED_BACK. - Notes:
[SME-PROPOSED]per D-006 from EP-005C-rtm.md: "Rollback SHALL be rejected withcode=NIGHT_AUDIT_ROLLBACK_WINDOW_EXPIRED(HTTP 409) whenDate.now() - completedAt > 2 hours. SUPER_ADMIN MAY override via explicit?force=truequery param + reason; override emitsnight.audit.rollback.forcedaudit-log entry." Plus the missingnight.audit.rolled_backevent (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
| # | Scenario | Expected behaviour | Coverage today | Owning story |
|---|---|---|---|---|
| F1 | Two CronJob ticks overlap (previous run hung > 24h, next 02:00 IST fires) | concurrencyPolicy: Forbid skips the second tick; ops alert | K8s manifest validated; no integration test | US-005C-07 |
| F2 | Distributed lock contention — manual MANAGER trigger fires while in-process scheduler is mid-audit | Second start returns 409 with ConflictException; idempotency holds | night-audit.service.spec.ts concurrency-lock specs; flow-9-night-audit.spec.ts S6 (full-run path under FLOW9_FULL_AUDIT=true) — PASS | US-005-004 |
| F3 | Partial post failure — Folio row locked by frontdesk transaction mid-audit | Audit step fails inside Prisma transaction; ALL partial work rolls back; NightAuditLog row → FAILED; lock released | night-audit.service.spec.ts transaction-rollback specs; AP-T09-001 pinning test | US-005-004 |
| F4 | Cron 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 debug | Not tested; resource limits in CronJob requests: 32Mi / limits: 64Mi (curl pod only — frontdesk-service handles the work) | [SME-PROPOSED] US-005C-NEW-OBSERVABILITY |
| F5 | K8s 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 boot | night-audit.service.spec.ts GSA-133 stale-RUNNING recovery spec | US-005C-07 (calibration AC proposed in Step 4) |
| F6 | Downstream Kafka consumer crash (analytics NightAuditConsumer dies after outbox relay) | Audit completes successfully (outbox row marked PUBLISHED); consumer-side replay required — outbox/:id/replay endpoint reprocesses | night-audit-outbox.service.spec.ts + outbox-replay-endpoint.spec.ts | US-005-004 (AP-T09-002) |
| F7 | Business-date already advanced (idempotency check) — chain orchestrator retries a property | Duplicate-date guard catches COMPLETED row for (propertyId, businessDate); second call is no-op | night-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 |
| F8 | Rollback 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 break | chain-audit-orchestrator.service.spec.ts (chain orchestration); no cross-property rollback test | [SME-PROPOSED] US-005C-03-followup |
| F9 | INTERNAL_SERVICE_TOKEN Secret stale after JWT keypair rotation | Cron pod's curl gets 401 from frontdesk-service; Job fails; pod retries 2× per backoffLimit | B2 (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 |
| F10 | NIGHT_AUDIT_CRON_DISABLED=true not set in K8s mode — both schedulers fire | In-process scheduler + K8s CronJob both attempt — distributed lock catches the second; but unnecessary load on Redis | Documented in EP-005C-rtm SME notes; no integration test | US-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 viaEINVOICE_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_backevent (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/chainand renderGET /chain/status/:trackingIdis not started (drift carries forward from US-005C-06). - Property-level
nightAuditHourfor 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_sourceMV.
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.)
| ID | Type | Severity | Description | Surfaced by step |
|---|---|---|---|---|
| D-001 | Story-numbering-drift | P0 | Three 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-002 | Stale-review-vs-shipped-code | P0 | reviews/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-003 | Story-incomplete + India-hospitality-critical | P1 | DER 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-004 | Catalog-vs-code | P0 | EPIC 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-005 | Story-incomplete + India-hospitality-critical | P1 | No-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-006 | Story-incomplete + missing-guard | P1 | EPIC 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-007 | Story-vs-impl-design-mismatch | P2 | EPIC 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-008 | Story-incomplete + India-hospitality-critical | P1 | Cashier-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 gate | P0 | No 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-critical | P1 | Step 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 scalability | P1 | K8s 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 observability | P1 | Chain 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
| Step | Spans | Why cross-EPIC |
|---|---|---|
| 1 | EP-005C ↔ EP-005B (POS) ↔ EP-006 (HK) | Pre-audit reads open KOTs (POS) + dirty room status (HK) + open folios (frontdesk) via @hmp/service-clients. |
| 2 | EP-005C ↔ Infrastructure | K8s CronJob in hmp-infrastructure; in-process scheduler in hmp-frontdesk-service. The NIGHT_AUDIT_CRON_DISABLED flag is the seam. |
| 3 | EP-005C | Dry-run is internal to frontdesk-service. |
| 4 | EP-005C ↔ Shared Redis (@hmp/cache) | Distributed lock seam. |
| 5 | EP-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. |
| 6 | EP-005C ↔ EP-003 (Reservations) ↔ EP-014 (Notifications) | No-show flips Reservation.status; publishes reservation.no_show event for EP-014 email fan-out. |
| 7 | EP-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. |
| 8 | EP-005C ↔ EP-012 (Reporting) | DER PDF/CSV generation. Finance consumes via CSV import to Tally/Zoho. |
| 9 | EP-005C ↔ EP-005A (Folios) | Proposed cross-folio reconciliation invariant. |
| 10 | EP-005C ↔ EP-012B (Analytics) | Outbox → message bus → NightAuditConsumer → 5 MVs. AP-T09-002 closure. |
| 11 | EP-005C ↔ EP-018 (Chain admin) | Chain orchestrator backend shipped; EP-018 dashboard not started. |
| 12 | EP-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.