Skip to main content

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:

CodePropertyCityRoomsTenant
KN-T-MUMMumbai (Bandra)Maharashtra40KEYNOVA-TEST
KN-T-BLRBangalore (Whitefield)Karnataka60KEYNOVA-TEST
KN-T-DELDelhi (Aerocity)Delhi120KEYNOVA-TEST
KN-T-JAIJaipur (City Palace)Rajasthan80KEYNOVA-TEST
KN-T-GOAGoa (Calangute)Goa50KEYNOVA-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​

FileClassRole
src/night-audit/night-audit.module.tsNightAuditModuleDI registration; imports ScheduleModule.forRoot()
src/night-audit/night-audit.service.tsNightAuditServiceCore engine: startNightAudit, _runNightAudit, processNightAudit, resumeAudit, rollbackNightAudit, validatePreAudit, getPreAuditReport, getNightAuditStatus, getNightAuditHistory, getNightAuditById, advanceBusinessDate, resetHousekeepingForNextDay
src/night-audit/night-audit.controller.tsNightAuditControllerREST endpoints under /night-audit
src/night-audit/night-audit-scheduler.service.tsNightAuditSchedulerService@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.tsChainAuditOrchestratorServiceSequential per-tenant chain audit with topological-sort dependency ordering

Key concurrency mechanisms​

MechanismScopeOwner
Redis SET NX audit:lock:{propertyId} (30-min TTL)Per-property runtime lockNightAuditService.acquireAuditLock
DistributedLockService.withLock('cron:night-audit:{propertyId}', 30 min)Per-property cron lockNightAuditSchedulerService
nightAuditLog row with status IN ('RUNNING', 'COMPLETED')Per-property per-date dedup_runNightAudit
Prisma $transaction (60s timeout)Atomicity of step 1+2+3processNightAudit

Audit step sequence​

  1. Resolve property timezone (getPropertyLocalMidnight) — converts UTC now() to midnight in the property's IANA timezone, falling back to Asia/Kolkata.
  2. Acquire Redis lock — fail fast with 409 if another audit is already running.
  3. Create nightAuditLog row with status=RUNNING.
  4. Open Prisma interactive transaction (60s timeout).
  5. Step 1 — Room charge posting: findMany CHECKED_IN reservations (cap 1000), compute dailyRate = rateAmount / nights, gstRate = calculateGSTForRoom(dailyRate), write folioTransaction with transactionType=CHARGE, transactionCode=ROOM, increment folio totals.
  6. Step 2 — No-show processing: findMany CONFIRMED reservations with arrivalDate < today (cap 1000); updateMany to NO_SHOW; post first-night charge with transactionCode=NO_SHOW.
  7. Step 3 — KPI snapshot: room.count total + occupied; compute occupancy = occupied/total*100, adr = revenue/roomsPosted, revpar = revenue/total.
  8. Commit transaction — either all of step 5/6/7 succeed, or all roll back.
  9. Post-tx side effects (non-atomic): publish reservation.no_show Kafka events, run SafeguardService.check for room charges/no-show fees/POS reconciliation/ADR/lock-pre-audit, update nightAuditLog to COMPLETED|COMPLETED_WITH_ERRORS|FAILED, advance business date in property.settings, reset housekeeping (mark occupied rooms DIRTY, create DAILY_CLEANING tasks for tomorrow), publish night.audit.completed Kafka event.

Triggers​

TriggerEndpoint / CronCaller
Manual single-propertyPOST /api/night-audit/:propertyId/startDashboard, Postman
Manual chain (entire tenant)POST /api/night-audit/chain (header x-tenant-id)Dashboard chain audit screen
Resume failed auditPOST /api/night-audit/:id/resumeDashboard recovery flow
Rollback completed auditPOST /api/night-audit/:id/rollbackDashboard manager screen
Scheduled per-property@Cron(EVERY_HOUR) checks settings.nightAuditHour (default 2 AM local)NightAuditSchedulerService

Existing test inventory (counts, before E3)​

Spec fileit() blocksRun state
night-audit.service.spec.ts20FAILS TO COMPILE — see Critical Bug 1
night-audit.controller.spec.ts12FAILS TO COMPILE — same root cause
night-audit-scheduler.service.spec.ts2FAILS TO COMPILE — same root cause
chain-audit-orchestrator.service.spec.ts11FAILS TO COMPILE — same root cause
night-audit-gst.spec.ts6PASSES (pure unit, no Prisma client typing)
Total516 / 51 passing

The 45 broken specs do not have logic regressions — they fail because night-audit.service.ts references a pOSOrder model 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:

  1. The POSOrder Prisma model lives only in hmp-pos-service. The hmp-frontdesk-service Prisma schema does not declare it.
  2. The TypeScript build mostly compiles only because of package.json's build script || true fault tolerance. The 4 night-audit spec files refuse to compile, which is why Jest reports Test suites: 4 failed, 1 passed.
  3. At runtime in QA/prod, when safeguardConfig.nightAudit.reconcilePosRevenue is true, this call will throw Cannot read properties of undefined (reading 'aggregate') because the Prisma client has no pOSOrder accessor. 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 the night.audit.completed Kafka event will all be skipped).
  4. Worse: the audit log row is never updated to COMPLETED, so the dashboard will show the audit as RUNNING indefinitely until a manager rolls it back.
  5. Per CLAUDE.md cross-service convention, frontdesk-service must read POS data via @hmp/service-clients, not via direct Prisma. This is a STRICT-TODO that 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):

  1. Inject PosServiceClient from @hmp/service-clients.
  2. Call posServiceClient.getPosOrderTotal({ propertyId, fromDate: today, toDate: today + 1d, statuses: ['BILLED', 'PAID'] }).
  3. If the call fails (network, 5xx), log a warning and proceed without the reconciliation safeguard — do not block the audit.
  4. Move the POS reconciliation step inside the transaction's post-completion side-effect block but wrap in try/catch so a failure cannot abort the completion update.
  5. 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:

  • processNightAudit first 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 to FAILED — but the business date has already been advanced and the housekeeping reset has already run (those happen after processNightAudit returns 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:

  1. POST /api/night-audit/15000000-0000-4000-a000-000000000001/start (Mumbai).
  2. Wait for response (single transaction, expected < 5s for 8 reservations).

Expected outcome:

  • HTTP 201, response body has auditLogId, roomsPosted=8, noShowsProcessed=0, totalRevenue matches sum of rateAmount/nights for the 8 reservations.
  • Mumbai's nightAuditLog row is in status COMPLETED.
  • The other 4 properties have no nightAuditLog row created.
  • The other 4 properties' folio_transactions table has no new rows from this audit.
  • Mumbai property's settings.businessDate advances by 1 day.
  • Bangalore/Delhi/Jaipur/Goa property settings.businessDate is unchanged.

Metrics to verify:

  • hmp_night_audit_completed_total{propertyId="...mum..."} += 1
  • hmp_night_audit_completed_total{propertyId="...blr..."} unchanged
  • folio_transactions WHERE postingDate = today AND propertyId(via folio→reservation) = mumbai count = 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:

  1. POST /api/night-audit/chain with header x-tenant-id: 00000000-0000-4000-a000-000000000099.
  2. Wait for response.

Expected outcome:

  • HTTP 201, response body matches ChainAuditResult shape with totalProperties=5, completed=5, failed=0, skipped=0.
  • 5 separate nightAuditLog rows created, one per property, all COMPLETED.
  • 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.completed Kafka event published with propertiesAudited=5.

Metrics to verify:

  • hmp_night_audit_chain_completed_total += 1
  • 5 night.audit.completed Kafka 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:

  1. Open 5 curl commands, one per property, all targeting POST /api/night-audit/{propertyId}/start.
  2. Launch all 5 commands within a 100ms window (xargs -P 5 or 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 COMPLETED audit log for today.

Steps:

  1. POST /api/night-audit/{mumbaiId}/start again.

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_IN reservations (room charges should post).
    • 3 CONFIRMED reservations with arrivalDate = today - 1 (should become NO_SHOW).
    • 2 walk-ins created today (CHECKED_IN after 14:00) — should also post charges.
    • 4 CHECKED_OUT reservations from yesterday — should be ignored.
    • 1 CANCELLED reservation — should be ignored.

Steps:

  1. POST /api/night-audit/{bangaloreId}/start.

Expected outcome:

  • roomsPosted = 7 (5 checked-in + 2 walk-ins).
  • noShowsProcessed = 3.
  • folio_transactions filtered to today shows:
    • 7 rows with transactionCode = 'ROOM'.
    • 3 rows with transactionCode = 'NO_SHOW'.
  • The 4 checked-out reservations' folios are unchanged (they should already be in SETTLED status).
  • The cancelled reservation generates no folio transactions.
  • The 3 no-show reservations' status transitions: CONFIRMED → NO_SHOW, with noShowAt = now().
  • All 3 no-shows fire reservation.no_show Kafka 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 = bangalore increased 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_IN reservation.
  • 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 POSOrder rows in status BILLED totalling ₹4,500.

Steps:

  1. 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 against posOrders = ₹4,500, find variance = 0, not trigger the NA_POS_RECONCILIATION_MISMATCH safeguard.
  • 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_IN reservation with rateAmount = 200 and currency = 'USD' (not the property default INR).
  • Property's Property.currency = 'INR'.
  • An exchange rate row exists in ExchangeRateHistory for USD → INR at 84.0.

Steps:

  1. POST /api/night-audit/{goaId}/start.

Expected outcome:

Gap finding: the current processNightAudit does not consult the reservation currency or ExchangeRateHistory. It treats rateAmount as 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:

  1. Trigger POST /api/night-audit/{mumbaiId}/start → audit runs for date 2026-04-12.
  2. 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).
  3. Inspect both nightAuditLog.auditDate values.

Expected outcome:

  • Mumbai auditDate = 2026-04-12 00:00:00+05:30 (which is 2026-04-11 18:30 UTC).
  • Goa auditDate = 2026-04-12 00:00:00+07:00 (which is 2026-04-11 17:00 UTC).
  • Both are stored as UTC instants in postgres.
  • getPropertyLocalMidnight correctly 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_backend on the audit's connection).

Steps:

  1. POST /api/night-audit/{jaipurId}/start → expected to fail with a DB error.
  2. Inspect the audit log row — should be status = FAILED.
  3. Re-start postgres.
  4. POST /api/night-audit/{auditLogId}/resume.
  5. 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_transactions for 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_transactions table for Bangalore's tenant during the test window).

Steps:

  1. POST /api/night-audit/chain (header x-tenant-id).
  2. Inspect response.

Expected outcome:

  • result.completed = 4 (Mumbai, Delhi, Jaipur, Goa).
  • result.failed = 1 (Bangalore).
  • result.properties[bangalore].status = 'failed'.
  • result.properties[bangalore].error contains 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.completed Kafka event still fires with propertiesAudited = 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 hour on a real run.

Steps:

  1. Wait for NightAuditSchedulerService.handleNightAuditCron to fire at the top of the hour.
  2. Inspect logs for Starting night audit for Mumbai.
  3. Inspect nightAuditLog for a fresh row with performedBy = '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-service running with materialized view consumers enabled (Workstream C2).
  • Subscribed to Kafka topic night.audit.completed.

Steps:

  1. Run POST /api/night-audit/chain.
  2. Wait 60s for analytics consumer to drain.
  3. Query analytics service for daily KPIs.

Expected outcome:

  • hmp-analytics-service receives 5 night.audit.completed Kafka events.
  • The materialized view mv_daily_kpis has 5 new rows for today, one per property, with occupancy, adr, revpar matching 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:

  1. GET /api/night-audit/{mumbaiId}/validate.
  2. Inspect response.

Expected outcome:

  • HTTP 200.
  • warnings array 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 = false because 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:

  1. POST /api/night-audit/{auditLogId}/rollback.
  2. Inspect folio_transactions table.
  3. Inspect nightAuditLog row.

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 rollbackNightAudit does not update folio totals — see rollback gap below).
  • nightAuditLog.status = 'ROLLED_BACK'.
  • night.audit.rolled_back Kafka 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.

#ConcernLocationSeverity
1pOSOrder cross-service drift breaks compilenight-audit.service.ts:445P0
2advanceBusinessDate overwrites entire settings JSONnight-audit.service.ts:662-675P0
3Side-effect failure leaves audit log in RUNNINGnight-audit.service.ts:182-211P1
4rollbackNightAudit does not decrement folio totalsnight-audit.service.ts:536-576P1
5Multi-currency reservations not converted to property currencynight-audit.service.ts:267-301P2 (no test property uses multi-currency)
6POS reconciliation aggregate filter does not scope by propertyIdnight-audit.service.ts:434-453P0 — folioTransaction aggregate is not filtered by propertyId, so a 5-property tenant will compute the variance for ALL properties' folios pooled together
7pOSOrder aggregate has the same flaw — no propertyId filternight-audit.service.ts:445-451P0 (same as #6)
8The findMany take: 1000 cap silently truncates large hotels with > 1000 in-house reservationsnight-audit.service.ts:262, 308P2 — 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') for postingDate = 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​

CriterionStatus
Night audit flow mapped + documentedDONE — 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/failDONE — 51 total, 6 passing, 45 fail-to-compile
Manual test plan writtenDONE — see phase1-night-audit-manual-test-plan.md
New .spec.ts files for uncovered pathsNOT 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​

  1. Are the P0 bugs (pOSOrder cross-service drift, advanceBusinessDate settings overwrite, POS reconciliation cross-property leakage) on the production launch blocker list? They must be fixed before 2026-04-14.
  2. Should the rollbackNightAudit button be hidden in the dashboard until the folio totals decrement gap is fixed?
  3. Should the booking engine reject foreign-currency reservations until the night audit is currency-aware?
  4. 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.