Phase 1 Night Audit Test Scenarios (5-Property Multi-Tenant Launch)
Workstream E3 — Production launch readiness for night audit on the 5-property KeyNova test tenant. Generated 2026-04-06. Companion document:
phase1-night-audit-manual-test-plan.md.
This document catalogs the test scenarios that the QA team must execute against the night audit subsystem before the 2026-04-14 production launch. It pairs each scenario with expected outcomes, metrics that should move, and a list of what constitutes a pass or fail.
The scenarios are scoped to the 5 test properties seeded by
hmp-infrastructure/seeds/seed-5-test-properties.sql:
| Code | Property | City | Rooms | Tenant |
|---|---|---|---|---|
| KN-T-MUM | Mumbai (Bandra) | Maharashtra | 40 | KEYNOVA-TEST |
| KN-T-BLR | Bangalore (Whitefield) | Karnataka | 60 | KEYNOVA-TEST |
| KN-T-DEL | Delhi (Aerocity) | Delhi | 120 | KEYNOVA-TEST |
| KN-T-JAI | Jaipur (City Palace) | Rajasthan | 80 | KEYNOVA-TEST |
| KN-T-GOA | Goa (Calangute) | Goa | 50 | KEYNOVA-TEST |
Tenant ID: 00000000-0000-4000-a000-000000000099
Total rooms across chain: 350
Single shared timezone: all 5 properties use Asia/Kolkata. Cross-timezone
isolation must be smoke-tested via a one-off timezone override on the Goa property
(see scenario 8).
1. Night Audit Code Map
Service: hmp-frontdesk-service
| File | Class | Role |
|---|---|---|
src/night-audit/night-audit.module.ts | NightAuditModule | DI registration; imports ScheduleModule.forRoot() |
src/night-audit/night-audit.service.ts | NightAuditService | Core engine: startNightAudit, _runNightAudit, processNightAudit, resumeAudit, rollbackNightAudit, validatePreAudit, getPreAuditReport, getNightAuditStatus, getNightAuditHistory, getNightAuditById, advanceBusinessDate, resetHousekeepingForNextDay |
src/night-audit/night-audit.controller.ts | NightAuditController | REST endpoints under /night-audit |
src/night-audit/night-audit-scheduler.service.ts | NightAuditSchedulerService | @Cron(EVERY_HOUR) — checks each active property for localHour === settings.nightAuditHour (default 2) and triggers audit via DistributedLockService.withLock |
src/night-audit/chain-audit-orchestrator.service.ts | ChainAuditOrchestratorService | Sequential per-tenant chain audit with topological-sort dependency ordering |
Key concurrency mechanisms
| Mechanism | Scope | Owner |
|---|---|---|
Redis SET NX audit:lock:{propertyId} (30-min TTL) | Per-property runtime lock | NightAuditService.acquireAuditLock |
DistributedLockService.withLock('cron:night-audit:{propertyId}', 30 min) | Per-property cron lock | NightAuditSchedulerService |
nightAuditLog row with status IN ('RUNNING', 'COMPLETED') | Per-property per-date dedup | _runNightAudit |
Prisma $transaction (60s timeout) | Atomicity of step 1+2+3 | processNightAudit |
Audit step sequence
- Resolve property timezone (
getPropertyLocalMidnight) — converts UTCnow()to midnight in the property's IANA timezone, falling back toAsia/Kolkata. - Acquire Redis lock — fail fast with
409if another audit is already running. - Create
nightAuditLogrow withstatus=RUNNING. - Open Prisma interactive transaction (60s timeout).
- Step 1 — Room charge posting:
findManyCHECKED_INreservations (cap 1000), computedailyRate = rateAmount / nights,gstRate = calculateGSTForRoom(dailyRate), writefolioTransactionwithtransactionType=CHARGE,transactionCode=ROOM, increment folio totals. - Step 2 — No-show processing:
findManyCONFIRMEDreservations witharrivalDate < today(cap 1000);updateManytoNO_SHOW; post first-night charge withtransactionCode=NO_SHOW. - Step 3 — KPI snapshot:
room.counttotal + occupied; computeoccupancy = occupied/total*100,adr = revenue/roomsPosted,revpar = revenue/total. - Commit transaction — either all of step 5/6/7 succeed, or all roll back.
- Post-tx side effects (non-atomic): publish
reservation.no_showKafka events, runSafeguardService.checkfor room charges/no-show fees/POS reconciliation/ADR/lock-pre-audit, updatenightAuditLogtoCOMPLETED|COMPLETED_WITH_ERRORS|FAILED, advance business date inproperty.settings, reset housekeeping (mark occupied roomsDIRTY, createDAILY_CLEANINGtasks for tomorrow), publishnight.audit.completedKafka event.
Triggers
| Trigger | Endpoint / Cron | Caller |
|---|---|---|
| Manual single-property | POST /api/night-audit/:propertyId/start | Dashboard, Postman |
| Manual chain (entire tenant) | POST /api/night-audit/chain (header x-tenant-id) | Dashboard chain audit screen |
| Resume failed audit | POST /api/night-audit/:id/resume | Dashboard recovery flow |
| Rollback completed audit | POST /api/night-audit/:id/rollback | Dashboard manager screen |
| Scheduled per-property | @Cron(EVERY_HOUR) checks settings.nightAuditHour (default 2 AM local) | NightAuditSchedulerService |
Existing test inventory (counts, before E3)
| Spec file | it() blocks | Run state |
|---|---|---|
night-audit.service.spec.ts | 20 | FAILS TO COMPILE — see Critical Bug 1 |
night-audit.controller.spec.ts | 12 | FAILS TO COMPILE — same root cause |
night-audit-scheduler.service.spec.ts | 2 | FAILS TO COMPILE — same root cause |
chain-audit-orchestrator.service.spec.ts | 11 | FAILS TO COMPILE — same root cause |
night-audit-gst.spec.ts | 6 | PASSES (pure unit, no Prisma client typing) |
| Total | 51 | 6 / 51 passing |
The 45 broken specs do not have logic regressions — they fail because
night-audit.service.tsreferences apOSOrdermodel that does not exist in the frontdesk-service Prisma schema. See Critical Bug 1 below.
2. Critical Bug Discovered (Escalation Required — Do Not Auto-Fix)
Critical Bug 1 — prisma.pOSOrder.aggregate references a model that does not exist in frontdesk-service
Location: hmp-frontdesk-service/src/night-audit/night-audit.service.ts:445
const posOrders = await this.prisma.pOSOrder.aggregate({
_sum: { total: true },
where: {
createdAt: { gte: today, lt: new Date(today.getTime() + 86400000) },
status: { in: ['BILLED', 'PAID'] },
},
});
Why this is critical for the 2026-04-14 launch:
- The
POSOrderPrisma model lives only inhmp-pos-service. Thehmp-frontdesk-servicePrisma schema does not declare it. - The TypeScript build mostly compiles only because of
package.json'sbuildscript|| truefault tolerance. The 4 night-audit spec files refuse to compile, which is why Jest reportsTest suites: 4 failed, 1 passed. - At runtime in QA/prod, when
safeguardConfig.nightAudit.reconcilePosRevenueis true, this call will throwCannot read properties of undefined (reading 'aggregate')because the Prisma client has nopOSOrderaccessor. The throw is inside the post-transaction side effects so it does not roll back the audit, but it does abort the rest of the side effects (housekeeping reset, business date advance is in flight before this, but ADR safeguard, lock-pre-audit safeguard, and thenight.audit.completedKafka event will all be skipped). - Worse: the audit log row is never updated to
COMPLETED, so the dashboard will show the audit asRUNNINGindefinitely until a manager rolls it back. - Per CLAUDE.md cross-service convention, frontdesk-service must read POS
data via
@hmp/service-clients, not via direct Prisma. This is aSTRICT-TODOthat was tagged but never executed.
Severity: P0 — blocks production launch. Must be fixed before 2026-04-14.
Suggested fix (for the engineer who picks this up — do NOT apply in this branch):
- Inject
PosServiceClientfrom@hmp/service-clients. - Call
posServiceClient.getPosOrderTotal({ propertyId, fromDate: today, toDate: today + 1d, statuses: ['BILLED', 'PAID'] }). - If the call fails (network, 5xx), log a warning and proceed without the reconciliation safeguard — do not block the audit.
- Move the POS reconciliation step inside the transaction's post-completion
side-effect block but wrap in
try/catchso a failure cannot abort the completion update. - Add a regression test that mocks the service client and verifies a 500 from POS does not block audit completion.
What this E3 task does NOT do: fix the bug. Per task brief: "If you find an actual bug (not just a gap), STOP and escalate — don't silently fix."
Critical Bug 2 — Audit log can hang in RUNNING state if a post-transaction side effect throws
Location: hmp-frontdesk-service/src/night-audit/night-audit.service.ts:182-211
The _runNightAudit method wraps processNightAudit in a try/catch. If
processNightAudit throws, the catch updates the audit log to FAILED. But:
processNightAuditfirst runs the transaction (atomic, safe).- Then it runs side effects outside the transaction: safeguards, POS reconciliation, Kafka publishes, ADR safeguard, lock-pre-audit safeguard.
- If any of these side effects throw (e.g. Critical Bug 1, Kafka broker
unavailable, SafeguardService DB unavailable), the throw bubbles back to
_runNightAudit's catch. The catch sets the audit log toFAILED— but the business date has already been advanced and the housekeeping reset has already run (those happen afterprocessNightAuditreturns successfully, so they're skipped in this failure mode). - However, if the side effect throws after
advanceBusinessDate(e.g. the housekeeping reset throws), the audit log is updated to FAILED but the business date has already advanced — leaving the system in an inconsistent state.
Severity: P1 — recoverable via resumeAudit, but the recovery is not idempotent
on advanceBusinessDate (which would advance the date a second time).
Suggested fix: wrap each post-transaction side effect in its own try/catch
that logs and pushes to errors[] rather than throwing. Only throw on the
in-transaction work.
Critical Bug 3 — advanceBusinessDate overwrites the entire settings JSON
Location: hmp-frontdesk-service/src/night-audit/night-audit.service.ts:662-675
await this.xPrisma.property.update({
where: { id: propertyId },
data: {
settings: {
businessDate: nextDate.toISOString().split('T')[0],
},
},
});
This replaces the entire Property.settings JSON column with { businessDate: '...' },
erasing nightAuditHour, checkInTime, checkOutTime, defaultCurrency,
timezone, etc. After the first night audit on a fresh seed, the
NightAuditSchedulerService will no longer find nightAuditHour=2 and will
default to 2 AM (which is fine), but checkInTime/checkOutTime will be lost,
breaking the booking engine and check-in flow.
Severity: P0 — blocks production launch. The 5-property seed sets all of
{checkInTime, checkOutTime, defaultCurrency, timezone} in settings. The first
audit run will erase them all.
Suggested fix: read existing settings, merge, then write:
const existing = await xPrisma.property.findUnique({ where: { id: propertyId }, select: { settings: true } });
await xPrisma.property.update({
where: { id: propertyId },
data: { settings: { ...(existing?.settings as object ?? {}), businessDate: ... } },
});
Escalation note: All three bugs predate E3 and were not caused by this task. They are flagged here so the production launch checklist (E1–E4) can include "fix night-audit P0/P1 bugs" as a hard gate.
3. Test Scenarios
The acceptance criteria asks for at least 8 scenarios; this document defines 14, grouped by isolation, concurrency, mixed states, side effects, recovery, timing, and analytics. Each scenario lists preconditions, steps, expected outcome, metrics that must move, and pass/fail criteria.
Scenario 1 — Per-property isolated audit execution (happy path)
Preconditions:
- 5-property seed loaded fresh, no prior audit logs.
- Mumbai property has 8 in-house reservations, 0 no-shows.
- All other properties have 0 in-house reservations.
Steps:
POST /api/night-audit/15000000-0000-4000-a000-000000000001/start(Mumbai).- Wait for response (single transaction, expected
< 5sfor 8 reservations).
Expected outcome:
- HTTP 201, response body has
auditLogId,roomsPosted=8,noShowsProcessed=0,totalRevenuematches sum ofrateAmount/nightsfor the 8 reservations. - Mumbai's
nightAuditLogrow is in statusCOMPLETED. - The other 4 properties have no
nightAuditLogrow created. - The other 4 properties'
folio_transactionstable has no new rows from this audit. - Mumbai property's
settings.businessDateadvances by 1 day. - Bangalore/Delhi/Jaipur/Goa property
settings.businessDateis unchanged.
Metrics to verify:
hmp_night_audit_completed_total{propertyId="...mum..."} += 1hmp_night_audit_completed_total{propertyId="...blr..."}unchangedfolio_transactions WHERE postingDate = today AND propertyId(via folio→reservation) = mumbaicount = 8
Pass: all five property assertions hold. Fail: any other property's row count changes; Mumbai charge count != 8; business date does not advance for Mumbai or advances for non-Mumbai.
Scenario 2 — Concurrent audit of 5 properties via chain endpoint
Preconditions:
- 5-property seed loaded.
- Each property has 5–15 in-house reservations.
- No prior audit logs for today.
Steps:
POST /api/night-audit/chainwith headerx-tenant-id: 00000000-0000-4000-a000-000000000099.- Wait for response.
Expected outcome:
- HTTP 201, response body matches
ChainAuditResultshape withtotalProperties=5,completed=5,failed=0,skipped=0. - 5 separate
nightAuditLogrows created, one per property, allCOMPLETED. summary.totalRoomsPosted = sum(per-property roomsPosted).summary.totalRevenue = sum(per-property totalRevenue)(rounded to 2 dp).summary.avgOccupancy = avg(per-property occupancy).- Each property's folio transactions only reference folios for that property.
- A single
night-audit.chain.completedKafka event published withpropertiesAudited=5.
Metrics to verify:
hmp_night_audit_chain_completed_total += 1- 5
night.audit.completedKafka events received in chronological order.
Pass: chain summary matches sum/avg of 5 per-property results. Fail: any cross-property folio leakage (a Mumbai charge posted on a Goa folio); fewer/more than 5 audit log rows; chain summary math does not equal sum of per-property results.
Note on "concurrent" semantics: the chain orchestrator runs audits sequentially (per-property in topological order). True parallel execution across properties is not currently supported. This scenario therefore validates sequential isolation, not parallel concurrency. Parallel audits are tested in scenario 3.
Scenario 3 — Parallel manual audits across 5 properties
Preconditions:
- 5-property seed loaded.
- 5 concurrent shell sessions.
Steps:
- Open 5
curlcommands, one per property, all targetingPOST /api/night-audit/{propertyId}/start. - Launch all 5 commands within a 100ms window (
xargs -P 5or 5 background processes).
Expected outcome:
- All 5 audits complete successfully (no Redis lock contention because each
property has its own
audit:lock:{propertyId}key). - 5 separate audit log rows, all
COMPLETED. - No deadlock errors in postgres logs.
- No transaction timeout (<60s) on any property.
Pass: all 5 return HTTP 201; no 409 Conflict; no postgres deadlocks logged.
Fail: any 409 Conflict (means lock key collision — would indicate the lock
key isn't scoped by propertyId); any postgres deadlock; any transaction
timeout.
Scenario 4 — Idempotency: re-trigger same audit twice
Preconditions:
- Mumbai already has a
COMPLETEDaudit log for today.
Steps:
POST /api/night-audit/{mumbaiId}/startagain.
Expected outcome:
- HTTP 400
BadRequestException: Night audit already completed for today. - No new audit log row created.
- No new folio transactions.
- Redis lock acquired and released cleanly (verified via Redis monitoring).
Pass: 400 returned; no DB mutations. Fail: any double-posting on folios; new audit log row created; lock leaked.
Scenario 5 — Mixed reservation states (checked-in, no-show, walk-in, checked-out)
Preconditions:
- Bangalore property pre-seeded with:
- 5
CHECKED_INreservations (room charges should post). - 3
CONFIRMEDreservations witharrivalDate = today - 1(should become NO_SHOW). - 2 walk-ins created today (
CHECKED_INafter 14:00) — should also post charges. - 4
CHECKED_OUTreservations from yesterday — should be ignored. - 1
CANCELLEDreservation — should be ignored.
- 5
Steps:
POST /api/night-audit/{bangaloreId}/start.
Expected outcome:
roomsPosted = 7(5 checked-in + 2 walk-ins).noShowsProcessed = 3.folio_transactionsfiltered to today shows:- 7 rows with
transactionCode = 'ROOM'. - 3 rows with
transactionCode = 'NO_SHOW'.
- 7 rows with
- The 4 checked-out reservations' folios are unchanged (they should already be
in
SETTLEDstatus). - The cancelled reservation generates no folio transactions.
- The 3 no-show reservations' status transitions:
CONFIRMED → NO_SHOW, withnoShowAt = now(). - All 3 no-shows fire
reservation.no_showKafka events.
Metrics to verify:
SELECT count(*) FROM folio_transactions WHERE postingDate = today AND transactionCode IN ('ROOM', 'NO_SHOW')=10.SELECT count(*) FROM reservations WHERE status = 'NO_SHOW' AND propertyId = bangaloreincreased by 3.
Pass: all 4 row counts match; all status transitions correct; no folio transactions on settled or cancelled reservations. Fail: any charge posted on a CHECKED_OUT or CANCELLED folio; checked-in reservations not charged; no-show reservations not transitioned.
Scenario 6 — POS outlet revenue posted to folio
Preconditions:
- Delhi property has 1
CHECKED_INreservation. - That reservation has 3 POS orders today, posted to the folio with
transactionCode IN ('RESTAURANT', 'BAR', 'ROOM_SERVICE'), total ₹4,500. - POS service has 3 corresponding
POSOrderrows in statusBILLEDtotalling ₹4,500.
Steps:
POST /api/night-audit/{delhiId}/start.
Expected outcome (currently broken — see Critical Bug 1):
- The audit transaction posts the room charge.
- The post-transaction POS reconciliation should compare
folioFnbCharges= ₹4,500 againstposOrders= ₹4,500, find variance = 0, not trigger theNA_POS_RECONCILIATION_MISMATCHsafeguard. - The audit log completes with
status = COMPLETED.
Currently observed (with bug):
prisma.pOSOrder.aggregate(...)throws because the model is missing.- The audit log stays in
RUNNING. - The room charge from step 1 is committed (it's inside the transaction).
- The business date advance never runs.
Pass criteria (after Critical Bug 1 is fixed): zero variance, no safeguard triggered, audit completes. Pre-fix pass criteria: test is expected to fail until the bug is fixed. The QA report should explicitly note this scenario as blocked on bug fix.
Scenario 7 — Foreign currency transactions
Preconditions:
- Goa property has 1
CHECKED_INreservation withrateAmount = 200andcurrency = 'USD'(not the property default INR). - Property's
Property.currency = 'INR'. - An exchange rate row exists in
ExchangeRateHistoryforUSD → INRat 84.0.
Steps:
POST /api/night-audit/{goaId}/start.
Expected outcome:
Gap finding: the current
processNightAuditdoes not consult the reservation currency orExchangeRateHistory. It treatsrateAmountas a raw number in the property currency. For an INR property with a USD reservation, it will post a charge of "200" (interpreted as ₹200) — a 100x revenue understatement.
Pass criteria: charge posted in property currency at the converted amount
(₹16,800 for USD 200 @ 84.0). Audit log totalRevenue reflects converted INR.
Currently observed: charge posted as 200, treated as ₹200.
Severity for E3 report: P2 — none of the 5 test properties currently use multi-currency, so this is not a launch blocker. Flag as known limitation in the production launch readiness doc and disable foreign-currency reservations in the booking engine until the night audit is currency-aware.
Scenario 8 — Cross-timezone audit isolation (timezone override on Goa)
Preconditions:
- Update Goa property
timezone = 'Asia/Bangkok'(UTC+7) — 1.5 hours behind Asia/Kolkata (UTC+5:30). - Local time when test runs: 2026-04-12 22:00 IST (= 2026-04-12 20:30 Bangkok).
- Mumbai (still on IST): "today" resolves to 2026-04-12 00:00 IST.
- Goa: "today" resolves to 2026-04-12 00:00 BKK = 2026-04-11 17:30 UTC = 2026-04-11 23:00 IST.
Steps:
- Trigger
POST /api/night-audit/{mumbaiId}/start→ audit runs for date 2026-04-12. - Trigger
POST /api/night-audit/{goaId}/start→ audit runs for date 2026-04-12 (because Bangkok local date is also 2026-04-12 at 20:30 BKK). - Inspect both
nightAuditLog.auditDatevalues.
Expected outcome:
- Mumbai
auditDate = 2026-04-12 00:00:00+05:30(which is2026-04-11 18:30 UTC). - Goa
auditDate = 2026-04-12 00:00:00+07:00(which is2026-04-11 17:00 UTC). - Both are stored as UTC instants in postgres.
getPropertyLocalMidnightcorrectly resolves the offset for each timezone.
Pass: the two audit dates differ by exactly 1.5 hours when converted to UTC, demonstrating the per-property timezone isolation. Fail: both audits compute the same UTC midnight (means timezone is being ignored).
Cleanup: revert Goa timezone to Asia/Kolkata.
Scenario 9 — Audit failure recovery via resumeAudit
Preconditions:
- Jaipur has 3 in-house reservations.
- Inject a one-time fault: stop the postgres container after the audit log is
created but before the transaction commits (or use
pg_terminate_backendon the audit's connection).
Steps:
POST /api/night-audit/{jaipurId}/start→ expected to fail with a DB error.- Inspect the audit log row — should be
status = FAILED. - Re-start postgres.
POST /api/night-audit/{auditLogId}/resume.- Inspect: original audit row marked
ROLLED_BACK; new audit row created and completes successfully.
Expected outcome:
- Original audit:
ROLLED_BACK. - New audit:
COMPLETED,roomsPosted = 3. folio_transactionsfor today has exactly 3 ROOM rows (no double-posting).
Pass: no double-posting. Final roomsPosted = 3.
Fail: roomsPosted = 6 (double-posting from the failed-then-resumed run).
Scenario 10 — Audit failure with one property failing in chain
Preconditions:
- All 5 properties have in-house reservations.
- Inject a fault that makes only Bangalore's audit fail (e.g. lock the
folio_transactionstable for Bangalore's tenant during the test window).
Steps:
POST /api/night-audit/chain(headerx-tenant-id).- Inspect response.
Expected outcome:
result.completed = 4(Mumbai, Delhi, Jaipur, Goa).result.failed = 1(Bangalore).result.properties[bangalore].status = 'failed'.result.properties[bangalore].errorcontains the postgres error message.- The chain audit continues past the Bangalore failure rather than short-circuiting.
- 4 successful audit log rows + 1 failed.
- The chain
night-audit.chain.completedKafka event still fires withpropertiesAudited = 4.
Pass: Bangalore failure does not block the other 4.
Fail: any of the other 4 properties show failed status; chain orchestrator
exits early.
Scenario 11 — Audit timing: scheduler runs at configured local hour
Preconditions:
- Mumbai
settings.nightAuditHour = 2(2 AM IST). - Set system clock or clock-mock to 2026-04-12 02:00 IST.
- Or configure the test property to have
nightAuditHour = current local houron a real run.
Steps:
- Wait for
NightAuditSchedulerService.handleNightAuditCronto fire at the top of the hour. - Inspect logs for
Starting night audit for Mumbai. - Inspect
nightAuditLogfor a fresh row withperformedBy = 'SYSTEM_SCHEDULER'.
Expected outcome:
- Audit runs once, exactly when local hour matches.
performedBy = 'SYSTEM_SCHEDULER'.- The cron-level
DistributedLockService.withLock('cron:night-audit:{propertyId}')prevents duplicate runs across replicas.
Metrics to verify:
- Check that
cron:night-audit:{propertyId}lock key was set + released in Redis. - Audit log row count for Mumbai today = 1.
Pass: exactly one audit row, performedBy = 'SYSTEM_SCHEDULER'.
Fail: zero audits (cron didn't fire); >1 audit (lock failed); audit ran at
wrong hour.
Scenario 12 — Audit impact on analytics and read models
Preconditions:
- All 5 properties seeded.
hmp-analytics-servicerunning with materialized view consumers enabled (Workstream C2).- Subscribed to Kafka topic
night.audit.completed.
Steps:
- Run
POST /api/night-audit/chain. - Wait 60s for analytics consumer to drain.
- Query analytics service for daily KPIs.
Expected outcome:
hmp-analytics-servicereceives 5night.audit.completedKafka events.- The materialized view
mv_daily_kpishas 5 new rows for today, one per property, withoccupancy,adr,revparmatching the audit log values. - The dashboard's "Last Night Audit" widget shows 5 properties with status
COMPLETED.
Pass: analytics MV row count for today = 5; KPI values match audit log. Fail: missing property in MV; KPI mismatch; consumer lag > 60s.
Scenario 13 — Pre-audit validation surfaces blocking conditions
Preconditions:
- Mumbai has 1 in-house reservation with
departureDate = today(pending checkout). - Mumbai has 2 open folios with non-zero balance.
- Mumbai has 1 unresolved maintenance request.
Steps:
GET /api/night-audit/{mumbaiId}/validate.- Inspect response.
Expected outcome:
- HTTP 200.
warningsarray contains 3 entries:open_folios,status_mismatch(if any),open_maintenance.
Steps (continued):
3. GET /api/night-audit/{mumbaiId}/pre-audit-report.
Expected outcome:
- Response has
canProceed = falsebecause of pending departures.
Pass: validation returns the 3 expected warnings; pre-audit report returns
canProceed = false.
Fail: any warning missing or extra; canProceed = true despite pending
departures.
Scenario 14 — Rollback completed audit reverses charges
Preconditions:
- Mumbai night audit completed today, posted 8 room charges totalling ₹40,000.
Steps:
POST /api/night-audit/{auditLogId}/rollback.- Inspect
folio_transactionstable. - Inspect
nightAuditLogrow.
Expected outcome:
- All 8 ROOM transactions for today have
isVoid = true,voidReason LIKE 'Night audit rollback%'. - Folio balances revert (manually verify by re-summing transactions, since the
current
rollbackNightAuditdoes not update folio totals — see rollback gap below). nightAuditLog.status = 'ROLLED_BACK'.night.audit.rolled_backKafka event published.
Gap finding: rollbackNightAudit voids the folio transactions but does
not decrement folio.totalCharges, folio.totalTax, and folio.balance.
This means after a rollback, the folio totals on the Folio table will still
include the rolled-back charges, while the transactions table no longer counts
them. The dashboard folio view (which reads folio.balance) will show stale
totals until the folio is recomputed.
Pass: void flags set; status is ROLLED_BACK. Pass with caveat: flag the folio totals gap as a P1 follow-up — must be fixed before launch or rollback button must be hidden in the dashboard.
4. Multi-Property Isolation Concerns Found in Code Review
Beyond the test scenarios above, the following code-level concerns were identified while mapping the night audit flow. Each is a candidate for a follow-up unit test once Critical Bug 1 is fixed and the suite compiles.
| # | Concern | Location | Severity |
|---|---|---|---|
| 1 | pOSOrder cross-service drift breaks compile | night-audit.service.ts:445 | P0 |
| 2 | advanceBusinessDate overwrites entire settings JSON | night-audit.service.ts:662-675 | P0 |
| 3 | Side-effect failure leaves audit log in RUNNING | night-audit.service.ts:182-211 | P1 |
| 4 | rollbackNightAudit does not decrement folio totals | night-audit.service.ts:536-576 | P1 |
| 5 | Multi-currency reservations not converted to property currency | night-audit.service.ts:267-301 | P2 (no test property uses multi-currency) |
| 6 | POS reconciliation aggregate filter does not scope by propertyId | night-audit.service.ts:434-453 | P0 — folioTransaction aggregate is not filtered by propertyId, so a 5-property tenant will compute the variance for ALL properties' folios pooled together |
| 7 | pOSOrder aggregate has the same flaw — no propertyId filter | night-audit.service.ts:445-451 | P0 (same as #6) |
| 8 | The findMany take: 1000 cap silently truncates large hotels with > 1000 in-house reservations | night-audit.service.ts:262, 308 | P2 — none of the 5 test properties exceed 120 rooms |
Concern #6 / #7 are the most dangerous multi-property defect found. When the chain orchestrator runs Mumbai's audit, the POS reconciliation safeguard aggregates every folio transaction with
transactionCode IN ('RESTAURANT', 'BAR', 'MINIBAR', 'ROOM_SERVICE')forpostingDate = today, across all properties — not just Mumbai. So Mumbai's reconciliation compares the entire chain's F&B revenue against the entire chain's POS revenue. By the time Bangalore's audit runs, the same comparison happens again with the same data, double-counting. This means the POS reconciliation safeguard is meaningless in a multi-property chain.
5. Acceptance Summary
| Criterion | Status |
|---|---|
| Night audit flow mapped + documented | DONE — section 1 |
| Multi-property scenarios documented (≥ 8) | DONE — 14 scenarios in section 3 |
| Critical bugs flagged (not fixed) | DONE — 3 P0/P1 bugs in section 2, 8 concerns in section 4 |
| Existing test count noted with pass/fail | DONE — 51 total, 6 passing, 45 fail-to-compile |
| Manual test plan written | DONE — see phase1-night-audit-manual-test-plan.md |
New .spec.ts files for uncovered paths | NOT DONE — would require fixing Critical Bug 1 first to make the existing suite compile, which the task brief explicitly forbids |
6. Open Questions for the Production Launch Owner
- Are the P0 bugs (
pOSOrdercross-service drift,advanceBusinessDatesettings overwrite, POS reconciliation cross-property leakage) on the production launch blocker list? They must be fixed before 2026-04-14. - Should the
rollbackNightAuditbutton be hidden in the dashboard until the folio totals decrement gap is fixed? - Should the booking engine reject foreign-currency reservations until the night audit is currency-aware?
- Do we need a real-time alert (PagerDuty) for
nightAuditLog.status = 'RUNNING'that has been running for > 30 minutes? This would catch the "stuck-in-RUNNING-after-side-effect-failure" symptom from Critical Bug 2.