Skip to main content

EP-005B — Folios & Billing RTM

EPIC: EP-005B Folios & Billing (parent: EP-005 Front Desk Operations) Reconciled: 2026-05-24 Wave 1 / per-EPIC reconciliation following the PLAYBOOK pattern established by EP-002-rtm.md. Source of truth for "% passing": hmp-docs/docs/platform-registry/2026-05-16-13-flow-demo-readiness.md (worst-of-the-thirteen — flow-8 check-out + e-invoice was 0/15 PASS at the snapshot; substantial fixes have shipped since but the doc has not been re-measured).

Numbering note: this EPIC carries two parallel story-ID schemes — the catalog (01-epic-catalog.md) uses US-005-001..022 (front-desk umbrella) with three folio rows (US-005-003 Folio Mgmt, US-005-007 E-Invoice, US-005-017 Folio Transfer); the EPIC body splits sub-EPIC EP-005B's own table (US-005-03..06, US-005-11, US-005B-01..08). This RTM follows the EPIC body numbering because EP-005B is a sub-EPIC and its own table is the natural authoring index. Cross-references to the catalog IDs are inlined where present. The numbering split is recorded as D-001 (P0) below.

EP-005B vs EP-005C split: the catalog row US-005-004 Night Audit Automation (per reviews/EP-005/US-005-004-review.md) is NOT covered here — night audit belongs to EP-005C. The 4 folio-charging side-effects of night audit (room charge posting, OVERSTAY_NIGHT charge, NO_SHOW charge, rollback) are mentioned in passing because they hit FolioTransaction, but the story owner is EP-005C.

Status legend​

StatusMeaning
BuiltCode exists + tests exist + tests pass
Built-UntestedCode exists, no automated test coverage (or test exists but is a stub)
PartialCode exists for some acceptance criteria, gaps for others
StubCode is a placeholder / TODO / returns mock data
Not StartedNo code

Summary​

MetricCount
Total stories (EPIC body)13
Catalog claims "Built"9
Catalog claims "Partial"3
Catalog claims "Not Started"1
Verified-Built7 (US-005-03, -04, -05, -06, -11, US-005B-02, US-005B-07)
Built-Untested1 (US-005B-08 — folio PDF + email; PDF service has 1 spec, email-dispatch handler has 0)
Partial4 (US-005B-01 merge — built, partial test; US-005B-03 lock — manager-role check missing; US-005B-05 refund — Razorpay only; US-005B-06 UPI QR — generation works, auto-reconcile pending)
Stub0
Not Started1 (US-005B-04 company/city ledger — entity + endpoints partial; statement + aging gaps)
Drift findings9 (D-001..D-009; 4 P0 / 4 P1 / 1 P2)
[SME-PROPOSED] refinements22

US-005-03 — Folio Creation & Charges (catalog: US-005-003 Folio Management)​

Catalog status (US-005-003): Built (P0, 8 points) EPIC body status (US-005-03): Built (P0, 5 points) Verified status: Built

Code locations:

  • hmp-frontdesk-service/src/folios/folios.controller.ts — full controller: GET/POST /folios, GET /folios/daily-summary, GET /folios/:id, GET /reservations/:reservationId/folios, POST /folios/:id/charges, POST /folios/:id/payments, POST /folios/:id/adjustments, POST /folios/:id/tds, POST /folios/:id/settle, POST /folios/:id/split, POST /folios/:id/multi-split, POST /folios/transfer, POST /folios/void-transaction, GET /folios/:id/invoice, GET /folios/:id/invoice/pdf, POST /folios/merge
  • hmp-frontdesk-service/src/folios/folios.service.ts (~1300 lines) — createFolio, postCharge, postPayment, postAdjustment, transferCharges, voidTransaction, splitFolio, multiSplitFolio, settleFolio, mergeFolios, getNextInvoiceNumber (CANONICAL per frontdesk CLAUDE.md — NOT in InvoicePdfService), getDailySummary
  • hmp-frontdesk-service/src/folios/dto/folio.dto.ts — PostChargeDto (carries optional cgstAmount/sgstAmount/igstAmount/customerState/placeOfSupply — W2.A.1), PostPaymentDto, TransferChargeDto, VoidTransactionDto, CreateFolioDto, AdjustmentDto, TdsAdjustmentDto, SplitFolioDto, MultiSplitFolioDto, MergeFoliosDto
  • hmp-frontdesk-service/src/folios/tax-split.util.ts — intra/inter-state split helper
  • hmp-frontdesk-service/src/folios/consumers/pos-folio-charge.consumer.ts — POS kot.completed / order.closed → folio charge (AP-T07-004 GST split derivation)
  • hmp-frontdesk-service/src/folios/consumers/spa-folio-charge.consumer.ts — Spa charge → folio (cross-service)
  • hmp-frontdesk-service/src/folios/consumers/payment-captured.consumer.ts — payment-service payment.captured → folio payment (Task #11 wire-up, 2026-05-19)
  • hmp-frontdesk-service/src/folios/consumers/folio-charge-posting.consumer.ts — internal outbox publisher
  • hmp-frontdesk-service/src/folio-lock/folio-lock.service.ts — Redis distributed lock to prevent concurrent post on the same folio
  • hmp-web-dashboard/src/app/(dashboard)/front-desk/folio/[id]/page.tsx — folio page (charge / payment / void / transfer UI)
  • hmp-web-dashboard/src/app/(dashboard)/front-desk/folio/[id]/_components/EInvoiceStatusCard.tsx — IRN status card (Task #11 wire-up)
  • hmp-web-dashboard/src/app/(dashboard)/front-desk/folio/[id]/_components/RefundModal.tsx

Unit + integration tests:

  • hmp-frontdesk-service/src/folios/folios.service.spec.ts — broad coverage: createFolio, postCharge (GST 12% / 18%, quantity multiplier, tax-exempt / RCM force-zero per P0-5/P0-6), postPayment (negative-amount, receipt-number, balance update), settleFolio (zero-balance, reject with balance, reject already-settled), transferCharge (success, reject voided, reject non-open target), voidTransaction, postAdjustment
  • hmp-frontdesk-service/src/folios/folios-phase-b.service.spec.ts — phase B operations
  • hmp-frontdesk-service/src/folios/folios-split.service.spec.ts — split + multi-split
  • hmp-frontdesk-service/src/folios/invoice-numbering.spec.ts — per-property invoice prefix sequencing (P0-8)
  • hmp-frontdesk-service/src/folios/__tests__/folio-cross-tenant-denial.spec.ts — NEW (Task #2, 2026-05-23) — 9 cases asserting cross-tenant 404/403 on GET, POST charge, POST payment, transfer, settle. Status: shipped to master 32addb1-class commit chain; verify against npm test in branch
  • hmp-frontdesk-service/src/folios/__tests__/gsa-006-invoice-ownership.spec.ts — invoice-number ownership scope
  • hmp-frontdesk-service/src/folios/__tests__/gsa-067-igst-split.spec.ts — intra-vs-inter state GST split
  • hmp-frontdesk-service/src/folios/consumers/pos-folio-charge.consumer.spec.ts — 29 cases incl. AP-T07-002 tenant defense + AP-T07-004 GST derivation
  • hmp-frontdesk-service/src/folios/consumers/payment-captured.consumer.spec.ts — 20 cases for the Razorpay capture → folio payment path
  • hmp-frontdesk-service/src/folios/consumers/spa-folio-charge.consumer.spec.ts — spa cross-service
  • hmp-frontdesk-service/src/folios/consumers/folio-charge-posting.consumer.spec.ts — outbox-style internal consumer
  • hmp-frontdesk-service/src/folios/fu-55-folio-event-dual-emit.spec.ts — 8 cases on the canonical+legacy dual-emit (FU-55 2026-05-23)
  • hmp-frontdesk-service/src/folio-lock/folio-lock.service.spec.ts — distributed lock

Flow tests:

  • hmp-infrastructure/testing/flow-tests/src/flows/flow-7-pos-to-folio.spec.ts (POS order → folio room-charge, 6 scenarios) — at 2026-05-16 snapshot: 0/10 PASS (flow-7 worst-of-three for BF-004) (2026-05-16-13-flow-demo-readiness.md)
  • hmp-infrastructure/testing/flow-tests/src/flows/flow-8-check-out.spec.ts (folio fetch → payment → settle → checkout, 16 it() cases) — 0/15 PASS at 2026-05-16 snapshot (worst-of-thirteen) (2026-05-16-13-flow-demo-readiness.md)
  • hmp-infrastructure/testing/flow-tests/src/flows/pos-folio-charge.flow.spec.ts (POS → folio, 6 scenarios) — TBD (not in 13-flow doc explicitly)
  • hmp-infrastructure/testing/flow-tests/src/flows/idempotency-replay.flow.spec.ts — folio charge idempotency
  • hmp-infrastructure/testing/flow-tests/src/flows/gst-tax-flags.flow.spec.ts — tax-exempt + RCM force-zero

Real-backend smokes:

  • hmp-web-dashboard/e2e/real-backend/smoke-folio.real.spec.ts — folio page loads
  • hmp-web-dashboard/e2e/real-backend/smoke-billing.real.spec.ts — billing overview

E2E (Playwright):

  • hmp-web-dashboard/e2e/tests/front-desk-folio.spec.ts — folio page
  • hmp-web-dashboard/e2e/tests/billing-overview.spec.ts
  • hmp-web-dashboard/e2e/tests/billing-invoices.spec.ts

Use cases referencing this story:

  • none directly named "folio-charge" — closest are uc-tax-flags-on-reservation (P0-5/P0-6) and uc-tds-adjustment (P0-11) cited in frontdesk CLAUDE.md but not as YAML rows in use-cases/ directly. Surface as D-002 (P1) below — folio-charge has NO use-case YAML at all.

SME notes:

  • The canonical invoice-number generator is FoliosService.getNextInvoiceNumber (frontdesk CLAUDE.md Gotcha 0/W2.A.1; reaffirmed in memory project_2026-05-20-top-10-tracker-closure.md after the AP-T08-003 / EP-002-rtm-style "InvoicePdfService.generateAuto" assumption was found stale). Any review-doc text suggesting "PDF service generates auto-numbers" is post-2026-05-19 stale; the spec at invoice-pdf.service.spec.ts was re-aligned. Spec stories must not encode the PDF-service path.
  • GST split (W2.A.1 / AP-T07-001 / AP-T07-004) is finally end-to-end: producer (POS, Spa, manual) → consumer → DTO → schema columns → DB migration. folios.service.ts:postCharge now passes dto.cgstAmount/sgstAmount/igstAmount/customerState/placeOfSupply straight through; legacy callers fall through to schema default(0). Migration 20260523120000_add_folio_transaction_gst_split finally adds the columns to the DB (was schema-only drift for ~2 weeks).
  • Tax-exempt + RCM (P0-5/P0-6) force taxAmount=0 even if the caller supplied a non-zero value. This is Indian-GST-correct (SEZ / govt / diplomatic supply is zero-rated; RCM means recipient self-assesses so supplier issues without tax line). Story body does NOT mention either path.
  • Per-property invoice prefix (P0-8) — Property.code slug regex ^[A-Z0-9]{2,8}$ resolves the prefix; falls back to 6-char hash of propertyId. Replaces the hard-coded HMP/ prefix that the old review file still references.
  • FU-55 dual-emit — postCharge now emits BOTH legacy folio.charge.posted AND canonical folio.posted (with analytics-segmentation fields: segment, countryCode, roomTypeCode, ratePlanCode, roomNights, taxAmount). Without this, 5 analytics MVs were empty. Retirement of legacy event is a future Track 4 sweep.
  • Distributed lock (FolioLockModule) — Redis-backed; prevents concurrent CHARGE on the same folio. Story body does not mention concurrency contract.
  • Cross-service xPrisma — folios still read reservation/guest data via the slim cross-service read model; CLAUDE.md Gotcha 4 calls this out as STRICT-TODO. Cross-tenant denial spec (Task #2 2026-05-23) is now the regression guard.
  • TDS adjustment (P0-11) is a real Indian-corporate-billing feature: when a corporate buyer deducts tax-at-source under §194I (rent), §194J (professional fees), §194Q (>₹50L), §194C (contract), §194O (e-commerce), §194IA, §194IB — the hotel needs a credit ADJUSTMENT row that ties out to Form 26AS reconciliation. Section allow-list + TAN format ABCD12345E + FY format YYYY-YY are enforced. Story body does NOT mention TDS at all — surface as D-003 (P0 — books won't tie out without it).

Drift findings:

D-001 — [P0] [Catalog-vs-EPIC-body] The catalog (01-epic-catalog.md) carries 3 folio rows under EP-005 umbrella (US-005-003 Folio Management, US-005-007 E-Invoice, US-005-017 Folio Transfer); the EP-005B EPIC body carries 13 rows under a different scheme (US-005-03..06, US-005-11, US-005B-01..08). Both schemes are live in different docs. A reviewer asking "is US-005B-04 built?" gets no answer from the catalog; a reviewer asking "is US-005-003 built?" gets a stale "Built" with no test pointer for the new sub-stories. This is BF-008 / BF-005 pilot-blocking.

  • [SME-PROPOSED] Adopt EP-005B's US-005B-NN scheme as canonical for sub-EPIC stories, and rewrite the catalog's EP-005 row for US-005-003 to read US-005-003 Folio Management — see EP-005B sub-EPIC for US-005B-NN rows. Sub-EPIC story IDs must NOT also live in the parent EPIC catalog row — the two schemes silently disagree (catalog says US-005-003 is Built; the underlying US-005B-04 company-ledger is Not Started).
  • [SME-PROPOSED] Apply the same recipe to EP-005A (check-in / check-out), EP-005C (night audit), EP-005D (other) since they have the same numbering trap.

D-002 — [P1] [Story-incomplete + use-case-missing] CLOSED 2026-06-07: _data/use-cases/uc-folio-money-lifecycle.yaml now represents the core CHARGE / PAYMENT (incl. isAdvance ADVANCE tagging, #18) / SPLIT / TRANSFER / VOID / SETTLE journey, citing the backend unit specs, the FT-10 flow, and the assertive live E2E advance-deposit.test-env.spec.ts. The two SME-proposed deeper YAMLs below (per-scenario GST-split charge; multi-currency payment + refund) remain optional enhancements, not blockers. (Original finding: folio create + charge had NO use-case YAML in _data/use-cases/ — the closest matches, uc-tax-flags-on-reservation / uc-tds-adjustment, covered only the tax flags + TDS adjustment surfaces.)

  • [SME-PROPOSED] Author uc-folio-charge-with-gst-split.yaml covering: (1) staff posts intra-state charge → CGST+SGST split persisted; (2) staff posts inter-state charge → IGST persisted; (3) staff posts tax-exempt charge → taxAmount forced to 0; (4) staff posts RCM-applicable charge → taxAmount forced to 0; (5) concurrent posts gated by FolioLock.
  • [SME-PROPOSED] Author uc-folio-payment-with-multi-currency.yaml covering: cash/card/UPI/NEFT, foreign-currency conversion via CurrencyClientService, idempotency via paymentRef, refund (negative amount, P0-1/P0-3).

D-003 — [P0] [Story-incomplete + India-tax-critical] Story body for US-005-03 does NOT mention TDS adjustment. Indian corporate-billing reality: a TCS / Infosys / govt-PSU guest will deduct tax-at-source on payment; without a credit ADJUSTMENT row with structured {kind:'TDS', section, rate, tan, fy, cert} reference, the hotel's books cannot reconcile to Form 26AS at year-end. POST /folios/:id/tds is shipped + tested (P0-11) but the story is silent.

  • [SME-PROPOSED] Add AC: "Front-desk SHALL support posting a TDS adjustment via POST /folios/:id/tds with body {section, rate, tan, fy, certNumber?, notes?}. Allowed sections: 194I, 194IA, 194IB, 194J, 194Q, 194C, 194O. TAN MUST match ^[A-Z]{4}[0-9]{5}[A-Z]$. FY MUST match ^[0-9]{4}-[0-9]{2}$ (e.g. 2026-27). System posts a credit ADJUSTMENT row (transactionCode='TDS') with structured reference payload and decrements folio balance accordingly. Audit trail: postedBy populated from JWT user."

US-005-04 — Folio Payment Recording (catalog: subsumed under US-005-003)​

Catalog status: covered by US-005-003 row (Built) EPIC body status (US-005-04): Built (P0, 5 points) Verified status: Built

Code locations:

  • hmp-frontdesk-service/src/folios/folios.service.ts:postPayment — defense-in-depth: rejects 0/NaN/Infinity; positive=inbound, negative=outbound refund (P0-1/P0-3); overpayment safeguard; multi-currency conversion; atomic FolioTransaction(PAYMENT, negative amount) + Payment row + folio balance update
  • hmp-frontdesk-service/src/folios/folios.controller.ts:POST /folios/:id/payments — @Roles('SUPER_ADMIN','ADMIN','MANAGER','FRONT_DESK','ACCOUNTS','F_AND_B','RESERVATIONS') + @Audit({tableName:'folio_transactions'})
  • hmp-frontdesk-service/src/folios/consumers/payment-captured.consumer.ts — NEW (Task #11, 2026-05-19, frontdesk CLAUDE.md Gotcha 27) — subscribes to payment-service payment.captured Redis Streams topic, resolves folio via externalReferenceType=FOLIO|RESERVATION, posts payment via FoliosService.postPayment(dto, 'system:payment-captured-consumer', ...). Dedup via FolioTransaction.reference shape folio:{folioId}:payment-captured:{paymentId}. Tenant-mismatch defense on folio.reservation.property.tenantId.
  • hmp-frontdesk-service/src/payments/ — internal payment-record CRUD
  • hmp-web-dashboard/src/app/(dashboard)/front-desk/folio/[id]/page.tsx — payment-record UI

Unit + integration tests:

  • hmp-frontdesk-service/src/folios/folios.service.spec.ts — postPayment cases incl. 0/NaN reject, negative refund (P0-1), overpayment trigger, multi-currency
  • hmp-frontdesk-service/src/folios/consumers/payment-captured.consumer.spec.ts — 20 cases covering all FOLIO/RESERVATION resolution paths, tenant-mismatch, providerCode mapping, dedup, SETTLED-folio race (XACK + log, no rethrow)
  • hmp-frontdesk-service/src/checkout/split-payment.spec.ts — split-payment lines at checkout (multi-mode settlement)

Flow tests:

  • hmp-infrastructure/testing/flow-tests/src/flows/flow-8-check-out.spec.ts (it: "POST /api/folios/:id/payments settles the outstanding balance") — at 2026-05-16 snapshot 0/15 PASS (2026-05-16-13-flow-demo-readiness.md)
  • hmp-infrastructure/testing/flow-tests/src/flows/advance-deposit-split.flow.spec.ts (5 scenarios — claim, 50% advance link, payable, terminal-charge remainder → zero, clean checkout) — TBD (2026-05-16-13-flow-demo-readiness.md)
  • hmp-infrastructure/testing/flow-tests/src/flows/terminal-folio-settlement.flow.spec.ts (6 scenarios — register, charge, decline+retry, cancel, refund, cross-tenant 404, FRONT_DESK ADMIN-only 403) — TBD
  • hmp-infrastructure/testing/flow-tests/src/flows/payments.flow.spec.ts — 8 scenarios for the payment-service surface; the payment.captured event is the trigger for PaymentCapturedConsumer

Real-backend smokes:

  • hmp-web-dashboard/e2e/real-backend/smoke-folio.real.spec.ts (shared with US-005-03)
  • hmp-web-dashboard/e2e/real-backend/smoke-payment-link.real.spec.ts — payment-link create

E2E (Playwright): covered by front-desk-folio.spec.ts

Use cases referencing this story: none (see D-002 above)

SME notes:

  • Payments are stored as negative amounts — this is by design for balance arithmetic (balance = totalCharges + sum(payments) where payments are signed). Refunds (P0-1/P0-3) use the same signed convention with transactionCode='REFUND' and skip the overpayment check. This is non-obvious for a new engineer — refund vs payment vs negative-payment is the kind of thing a spec MUST encode explicitly.
  • The new PaymentCapturedConsumer is the ONLY supported path for booking-engine redirect captures, terminal-charge captures, payment-link captures, and UPI-QR captures (all four producer paths emit payment.captured via the payment-service transactional outbox). The legacy direct-POST path is still wired for cash + manual entries from the dashboard. Story body says "record payments" — silent on the producer-driven path.
  • FOLIO_PAYMENT_RECEIVED carries transactionId (frontdesk CLAUDE.md Gotcha 24, 2026-05-05) — reservation-service FolioPaymentConsumer uses this as the Redis SETNX dedup key. Without it, falls back to a folioId:amount:balance composite key. Story doesn't pin the event-payload contract.
  • paymentRef idempotency — the EPIC body §3.1 lists "Idempotent payment creation" as in-scope; the DTO has paymentRef?: string but the service-layer enforcement is via the consumer's reference dedup pattern, not a DB unique constraint on Payment.paymentRef. Verify in spec.

Drift findings:

D-004 — [P1] [Story-incomplete + producer-side] EP-005B EPIC §3.1 says "Idempotent payment creation (prevent duplicate charge on retry)" but the AC under US-005-04 doesn't enforce a unique constraint or a service-layer guard on paymentRef. Today the consumer dedup happens at FolioTransaction.reference shape, NOT at Payment.paymentRef. A direct retry of POST /folios/:id/payments with the same paymentRef would create a duplicate Payment row.

  • [SME-PROPOSED] Add AC: "POST /folios/:id/payments SHALL reject (409 with code=PAYMENT_DUPLICATE_REF) any payment whose paymentRef matches an existing Payment row for the same folio + same payment-mode. The check SHALL be a unique partial index Payment(folioId, paymentMode, paymentRef) WHERE paymentRef IS NOT NULL enforced by Prisma at write time."
  • [SME-PROPOSED] Add AC: "The PaymentCapturedConsumer dedup key SHALL remain folio:{folioId}:payment-captured:{paymentId} (idempotent across XADD retries) — this is independent of the unique constraint above; both must hold."

US-005-05 — Folio Transfer & Split (catalog: US-005-017 Folio Transfer Between Guests)​

Catalog status (US-005-017): Built (P1, 3 points) EPIC body status (US-005-05): Built (P0, 5 points) Verified status: Built

Code locations:

  • hmp-frontdesk-service/src/folios/folios.service.ts — transferCharges, splitFolio, multiSplitFolio
  • hmp-frontdesk-service/src/folios/folios.controller.ts — POST /folios/transfer, POST /folios/:id/split, POST /folios/:id/multi-split
  • hmp-frontdesk-service/src/folios/dto/folio.dto.ts — TransferChargeDto, TransferChargesDto, SplitFolioDto, MultiSplitFolioDto
  • hmp-web-dashboard/src/app/(dashboard)/front-desk/folio/[id]/page.tsx — UI for transfer + split (Phase 5 deliverable per EPIC §4.2)

Unit + integration tests:

  • hmp-frontdesk-service/src/folios/folios-split.service.spec.ts — split + multi-split coverage
  • hmp-frontdesk-service/src/folios/folios.service.spec.ts — transferCharge cases (success, reject voided, reject non-open target, balance recalc)
  • hmp-frontdesk-service/src/folios/__tests__/folio-cross-tenant-denial.spec.ts — covers cross-tenant transfer / split denial

Flow tests:

  • hmp-infrastructure/testing/flow-tests/src/flows/advance-deposit-split.flow.spec.ts (split-payment scenarios) — TBD

Real-backend smokes: covered via smoke-folio.real.spec.ts

E2E (Playwright): covered via front-desk-folio.spec.ts

Use cases referencing this story: none

SME notes:

  • Splitting moves selected transactions to a new folio + creates TRANSFER-out on source + TRANSFER-in on target. Both balances recalculate atomically inside $transaction. Story body covers this in the EPIC §3.1 bullets but not in AC form.
  • Split-bill use case from EPIC §3.1 ("separate items to guest-pay vs. company-pay sub-folios") implies a transferRule driven by chargeType (e.g. "all minibar to guest, all room+telephone to company"). Today the implementation moves individual transaction IDs — no rule-based partitioning. A real-world finance user wants "move all charges with transactionCode IN ('MINIBAR','LAUNDRY','TELEPHONE') to folio B" in one call, not 15 separate POSTs.
  • Merge (US-005B-01 below) is the reverse operation — same balance-recalc invariants.
  • Transfer is voidable-charge-protected (rejects if source transaction is isVoid=true) and target-OPEN-protected.

Drift findings:

D-005 — [P1] [Story-incomplete + UX-gap] EPIC body says "split bills (US-005B-NNN for splits)" without enumerating the split rule API. Today the implementation requires the caller to enumerate transaction IDs; finance teams want rule-based split ({ rules: [{chargeTypes: ['MINIBAR'], targetFolio: 'B'}, ...] }).

  • [SME-PROPOSED] Add AC: "POST /folios/:id/split SHALL accept a rule-based splitRules: SplitRule[] body shape where each rule selects transactions by chargeTypes[], transactionCodes[], amountFilter? {min,max}, or explicit ids[]. Mixed mode allowed (multiple rules, evaluated in order, each transaction matches exactly one rule)."
  • [SME-PROPOSED] Add AC: "Split SHALL create a new folio inheriting the source's reservationId, propertyId, tenantId, currency and incremented folioNumber ({folioNumber}-S{n} shape). Audit trail: source folio receives a SPLIT_OUT audit row referencing the new folio ID."

US-005-06 — Folio Settlement​

Catalog status: covered by US-005-003 row (Built) EPIC body status (US-005-06): Built (P0, 5 points) Verified status: Built

Code locations:

  • hmp-frontdesk-service/src/folios/folios.service.ts:settleFolio — validates balance===0, sets status=SETTLED + settledAt=now(), publishes BOTH legacy folio.settled AND canonical folio.closed (FU-55 dual-emit 2026-05-23)
  • hmp-frontdesk-service/src/folios/folios.controller.ts:POST /folios/:id/settle
  • hmp-frontdesk-service/src/checkout/checkout.service.ts — settlement orchestration inside checkout pipeline (calls settleFolio per open folio)

Unit + integration tests:

  • hmp-frontdesk-service/src/folios/folios.service.spec.ts — settleFolio cases: zero-balance success, reject with balance, reject already-settled, reject voided folio
  • hmp-frontdesk-service/src/folios/fu-55-folio-event-dual-emit.spec.ts — 8 cases covering legacy + canonical event shapes + remap (totalAmount/closedAt/segment/countryCode/taxAmount)
  • hmp-frontdesk-service/src/checkout/checkout.service.spec.ts — settle-during-checkout
  • hmp-frontdesk-service/src/checkout/express-checkout.spec.ts — express path (auto-settle zero-balance)

Flow tests:

  • hmp-infrastructure/testing/flow-tests/src/flows/flow-8-check-out.spec.ts (it: "POST /api/folios/:id/settle closes the folio (status becomes SETTLED/CLOSED)", "folio status is SETTLED or CLOSED after settle call", "folio is SETTLED with zero balance after checkout") — 0/15 PASS at snapshot
  • hmp-infrastructure/testing/flow-tests/src/flows/check-out-einvoice.flow.spec.ts — same checkout-then-e-invoice path (FINDING-008 fix: IRN surfaced per settled folio)

Real-backend smokes: covered via smoke-folio.real.spec.ts + smoke-billing.real.spec.ts

E2E (Playwright): covered via front-desk-folio.spec.ts

Use cases referencing this story: none

SME notes:

  • Settlement is final — no new charges (Folio status invariant) — only recourse is a CreditNote (US-005B-02 below). Story body covers this in EPIC §3.1 narrative; spec should make it an explicit AC.
  • FU-55 dual-emit — both legacy folio.settled and canonical folio.closed are published (canonical adds segment, countryCode, remaps totalAmount/closedAt/taxAmount). Until Track 4 sweep, both must publish; consumers should subscribe to the canonical event only.
  • Settle-during-checkout — checkout.service.ts calls settleFolio per OPEN folio; FINDING-008 fix means each settled folio surfaces its IRN in the checkout response (verified in check-out-einvoice.flow.spec.ts). If a tenant is a demo tenant, the per-folio IRN is SKIPPED_DEMO_TENANT (frontdesk CLAUDE.md Gotcha 26 / Plan 4 2026-05-13).
  • Settlement vs zero balance — current code requires exact zero. Real-world: a 0.50 INR rounding residual should auto-write-off via a ROUNDING adjustment row. Today: agent must manually post a 0.50 adjustment then settle — friction at the most error-prone step of the day.

Drift findings:

D-006 — [P1] [Story-incomplete + UX-gap] Settle requires exact-zero balance. A 1-paise residual blocks settle and forces manual adjustment. Real-world hotels routinely produce 1-50 paise residuals from currency-conversion rounding (P0-1 negative-amount path) or 5% GST on a ₹1 charge.

  • [SME-PROPOSED] Add AC: "POST /folios/:id/settle?autoRound=true (default true for staff role; false for guest-self-service) SHALL auto-post a ROUNDING adjustment row absorbing any residual |balance| <= 1.00 INR (configurable per property: Property.settlementRoundingTolerance, default 1.00). The adjustment carries reference={kind:'ROUNDING', residual: <signed value>} for audit."

US-005-11 — GST E-Invoice Generation (catalog: US-005-007 E-Invoice Generation)​

Catalog status (US-005-007): Built (P0, 5 points) EPIC body status (US-005-11): Built (P0, 8 points) Verified status: Built (vastly more mature than the stale reviews/EP-005/US-005-007-review.md 2026-03-07 "PARTIAL 2/10" verdict — AP-T08-001/-003/-005/-006/-007 + demo-tenant guard + Folio.irn denorm + B2B GSTIN flow + queue-routed CRN have all shipped since)

Code locations:

  • hmp-frontdesk-service/src/einvoice/einvoice.controller.ts — POST /einvoice/generate, POST /einvoice/batch-generate (max 50), POST /einvoice/cancel, POST /einvoice/credit-note (AP-T08-007 queue-routed), GET /einvoice/jobs, GET /einvoice/jobs/:jobId, GET /einvoice/dead-letter, POST /einvoice/dead-letter/:jobId/retry, GET /einvoice/compliance-report (P0-16), GET /einvoice/:irn
  • hmp-frontdesk-service/src/einvoice/einvoice.service.ts — generateIrnForFolio, cancelIrn, getIrnDetails, generateCreditNote (queue-routed, AP-T08-007), getComplianceReport (P0-16 buckets: filed/queued/processing/deadLetter/missing)
  • hmp-frontdesk-service/src/einvoice/einvoice.module.ts — provider factory: EINVOICE_MOCK=true → MockAdapter; COMPLIANCE_MODE=live + creds → NicAdapter; else MockAdapter with warning. Throws at startup in NODE_ENV=production + COMPLIANCE_MODE=live without any of EINVOICE_CLIENT_ID/SECRET/USERNAME/PASSWORD/GSTIN (escape hatch: EINVOICE_ALLOW_MOCK_IN_PROD=true) — AP-T08-001 / 2026-05-19
  • hmp-frontdesk-service/src/einvoice/nic-einvoice.adapter.ts — /eivital/v1.04/auth, token cached until expiry, sandbox vs production URL resolution
  • hmp-frontdesk-service/src/einvoice/mock-einvoice.adapter.ts — SHA-256 64-char hex IRNs, structurally valid 15-digit ack numbers, Base64 QR payload
  • hmp-frontdesk-service/src/einvoice/einvoice-job.processor.ts — cron every minute, 5 jobs/batch, 5-min retry backoff, maxRetries=3, DLQ on exceed; documentType-aware completion (AP-T08-007): INV jobs write to Folio.irn/signedQRCode/ackDate/ackNumber (AP-T08-006); CRN jobs write to CreditNote.creditNoteIrn + status=IRN_GENERATED and MUST NOT touch Folio.irn
  • hmp-frontdesk-service/src/einvoice/einvoice-job.repository.ts — DB-backed queue CRUD; findActiveCreditNoteJob(folioId, creditNoteNumber) idempotency guard (matches QUEUED/PROCESSING/COMPLETED; ignores DEAD_LETTER/FAILED to allow re-queue)
  • hmp-frontdesk-service/src/einvoice/einvoice-webhook.controller.ts — NIC async callback
  • hmp-frontdesk-service/src/einvoice/eway-bill.service.ts — related GST compliance
  • hmp-frontdesk-service/src/demo-tenant/ — DemoTenantGuardService (Plan 4 2026-05-13): when Tenant.isDemoTenant=true, NIC submission is SKIPPED (no job row, no API call, structured DEMO_TENANT_EINVOICE_SKIPPED log) — stricter than EINVOICE_MOCK
  • hmp-frontdesk-service/src/folios/invoice-pdf.service.ts — reads folio.irn directly (AP-T08-006); renders "DRAFT — IRN PENDING" watermark when null; renders real IRN + QR when set
  • hmp-web-dashboard/src/app/(dashboard)/front-desk/folio/[id]/_components/EInvoiceStatusCard.tsx — IRN-status card (queued/processing/completed/failed/skipped-demo) — Task #11 wire-up

Unit + integration tests:

  • hmp-frontdesk-service/src/einvoice/einvoice.service.spec.ts — generateIrnForFolio, cancelIrn, getIrnDetails, generateCreditNote (queue-routing + idempotency — AP-T08-007), B2B GSTIN path (AP-T08-003)
  • hmp-frontdesk-service/src/einvoice/einvoice.module.spec.ts — 12 cases on factory resolution; 2 specs were stale post-AP-T08-003 (set only NIC_EINVOICE_CLIENT_ID but code now requires all 5 EINVOICE_* vars) — fixed in 2026-05-19-gold-standard-pass-shipped session (memory)
  • hmp-frontdesk-service/src/einvoice/einvoice-job.processor.spec.ts — INV-vs-CRN branch + retry + DLQ
  • hmp-frontdesk-service/src/einvoice/nic-einvoice.adapter.spec.ts — [CITED-BUT-NOT-FOUND] historically described as 2-trivial-tests; verify post-AP-T08-003 update
  • hmp-frontdesk-service/src/einvoice/mock-einvoice.adapter.spec.ts — mock IRN format + payload shape
  • hmp-frontdesk-service/src/folios/invoice-pdf.service.spec.ts — re-aligned after getNextInvoiceNumber ownership clarification (Top-10 closure 2026-05-20)

Flow tests:

  • hmp-infrastructure/testing/flow-tests/src/flows/flow-8-check-out.spec.ts (its: "POST /api/einvoice/generate is reachable", "...response contains a jobId and QUEUED status", "GET /api/einvoice/jobs/:jobId eventually reaches COMPLETED with a 64-char IRN", "completed job carries GST amounts", "POST /api/einvoice/generate a second time for the same folio returns the same IRN") — 0/15 PASS at snapshot
  • hmp-infrastructure/testing/flow-tests/src/flows/check-out-einvoice.flow.spec.ts (it: "checkout response surfaces an e-invoice IRN per settled folio (FINDING-008 fix)") — TBD
  • hmp-infrastructure/testing/flow-tests/src/flows/nic-einvoice-live.flow.spec.ts (4 scenarios — adapter-coexistence prod-swap safety, mock-vs-live envelope parity, refuse-to-boot-silently in live without creds, v1.04 endpoint pins) — TBD
  • hmp-infrastructure/testing/flow-tests/src/flows/gst-tax-flags.flow.spec.ts — tax-exempt + RCM paths

Real-backend smokes: covered via smoke-folio.real.spec.ts (IRN status card) + smoke-billing.real.spec.ts

E2E (Playwright): front-desk-folio.spec.ts covers the IRN status card

Use cases referencing this story:

  • uc-einvoice-generation-async.yaml [CITED-BUT-NOT-FOUND] — likely missing; verify in _data/use-cases/. Surface as proposed-new UC.

SME notes:

  • The 2026-03-07 review file is profoundly stale (verdict "PARTIAL 2/10") — every gap it identifies has shipped since. Concretely: SAC/HSN mapping (996311 room, 996331 F&B — einvoice CLAUDE.md), B2B/B2C branching (folio.company?.gstin triggers B2B via company include — AP-T08-003), GST split (CGST+SGST intra vs IGST inter — derived from GSTIN state codes), credit note (queue-routed CRN — AP-T08-007), QR code (mock adapter Base64 payload + NIC signedQRCode), PDF (InvoicePdfService reads folio.irn directly — AP-T08-006), retry/queue (cron every minute, 5-min backoff, DLQ — EInvoiceJobProcessor). Reviewer reading the review file alone would reopen closed work — exactly the EP-002 D-011 anti-pattern. Re-classify as Built.
  • Demo-tenant guard (Plan 4 2026-05-13) is the highest-impact India-compliance fix in the whole module — without it, demo bookings to a real GSTIN would file real B2B tax-liability against the tenant's GSTIN. The skip is observable only via a structured log line (no audit row); AUDIT GAP flagged in CLAUDE.md Gotcha 26 — consider follow-up.
  • B2B include is load-bearing (CLAUDE.md Gotcha 6 / AP-T08-003) — if a future refactor drops company: true from the generateIrnForFolio folio query, every corporate booking silently regresses to B2C with buyer.GSTIN='URP', breaking GSTR-1/GSTR-2B reconciliation for every corporate account. Spec MUST encode this as a regression guard.
  • Credit-note queue routing (AP-T08-007) is async-by-default — a NIC timeout no longer permanently loses the credit note. Idempotency via findActiveCreditNoteJob(folioId, creditNoteNumber) matches QUEUED/PROCESSING/COMPLETED. DEAD_LETTER/FAILED jobs are intentionally NOT matched so a permanently-failed CRN can be re-queued by calling again.
  • Folio.irn denorm (AP-T08-006) is (irn, signedQRCode, ackDate, ackNumber) columns on Folio populated by EInvoiceJobProcessor on COMPLETED. PDF reads from Folio, not EInvoiceJob — backfill script at prisma/migrations-data/backfill-folio-irn.sql for legacy rows.
  • Compliance report (P0-16) is the canonical daily-reconciliation dashboard — buckets folios by filed / queued / processing / deadLetter / missing for a date range. Ops MUST run this daily.
  • Story body uses "30 seconds" SLA on IRN generation (EPIC §4.1 AC) — but the queue is async with 5-min retry backoff. A retry on transient NIC outage will visibly miss the 30-sec SLA. Spec should clarify "p95 < 30s on first attempt; queued jobs have NO completion SLA, only reconciliation SLA."

Drift findings:

D-007 — [P0] [Stale-review + India-tax-critical + AUDIT-GAP] The 2026-03-07 reviews/EP-005/US-005-007-review.md says "PARTIAL 2/10" but every gap it lists has shipped. A reviewer trusting the review file would reopen ~10 closed work items — exactly the EP-002 D-011 anti-pattern. Additionally, the demo-tenant skip (Plan 4 2026-05-13) leaves NO EInvoiceJob row (observable only via structured log), so compliance evidence packs cannot prove "demo tenant — intentionally skipped" without parsing logs.

  • [SME-PROPOSED] Re-run US-005-007 / US-005-11 review against current state; refresh verdict to Built. Promote EInvoiceJob.status enum to add SKIPPED_DEMO_TENANT (with migration) so compliance evidence is queryable in SQL, not greppable in logs.
  • [SME-PROPOSED] Add AC: "When Tenant.isDemoTenant=true, generateIrnForFolio SHALL persist an EInvoiceJob row with status='SKIPPED_DEMO_TENANT' (NO NIC API call, NO IRN), so the daily compliance report can include a demoSkipped bucket without log parsing."

D-008 — [P0] [Story-incomplete + cross-service-handoff] EPIC §4.1 AC AC-EP-005B-02 says "IRN within 30 seconds" but the queue is async with 5-min retry backoff. A transient NIC outage will visibly violate the SLA. Story does not differentiate "first-attempt SLA" vs "reconciliation SLA". Operators will see the inconsistency on the dashboard and lose trust.

  • [SME-PROPOSED] Replace AC-EP-005B-02 with two separate ACs: (a) "p95 of first-attempt IRN generation SHALL complete within 30 seconds (mock-mode) or 90 seconds (NIC sandbox/live)"; (b) "Queued jobs in QUEUED/PROCESSING state SHALL be reconciled within 24 hours via getComplianceReport daily run; DEAD_LETTER jobs SHALL be reviewed within 1 business day with POST /einvoice/dead-letter/:jobId/retry."
  • [SME-PROPOSED] Author uc-einvoice-generation-async.yaml covering: queue lifecycle (QUEUED → PROCESSING → COMPLETED/FAILED → DEAD_LETTER), demo-tenant skip path, B2B vs B2C branching, credit-note queue routing, Folio.irn denorm on completion.

US-005B-01 — Folio Merge​

Catalog status: not enumerated (under US-005-003 row) EPIC body status: Built (P1, 5 points, Sprint 4) Verified status: Partial — POST /folios/merge route + service method ship per CLAUDE.md API table and folios.service.ts re-exports; spec coverage thin

Code locations:

  • hmp-frontdesk-service/src/folios/folios.controller.ts — POST /folios/merge
  • hmp-frontdesk-service/src/folios/folios.service.ts:mergeFolios — combines two folios (e.g. consecutive stays / shared room)
  • hmp-frontdesk-service/src/folios/dto/folio.dto.ts — MergeFoliosDto
  • hmp-web-dashboard/src/app/(dashboard)/front-desk/folio/[id]/page.tsx — Phase 5 folio-management UI for merge (per EPIC §4.2)

Unit + integration tests:

  • hmp-frontdesk-service/src/folios/folios-phase-b.service.spec.ts — phase B includes merge cases (cited in source structure; verify case count)
  • No dedicated folios-merge.spec.ts — gap

Flow tests: none specific

Real-backend smokes: none specific

Use cases referencing this story: none

SME notes:

  • Merge is the reverse of split — same balance-recalc invariants, but with an extra constraint: both folios must belong to the same reservation (or the same group master). EPIC body says "consecutive stays / shared room" — both imply same guest, which is enforceable.
  • Invariants worth pinning in a regression spec: (a) merged folio status is OPEN; (b) source folio status is TRANSFERRED + mergedIntoFolioId populated; (c) source folio transactions are NOT physically moved (audit trail) — instead, mirrored TRANSFER-out + TRANSFER-in pair; (d) totalCharges + totalPayments + totalTax recalculated on target; (e) source folio cannot receive new charges post-merge.
  • Cross-tenant denial — verify merge respects the same tenant-scope check as transfer.

Drift findings:

D-009 — [P2] [Test-coverage-gap] mergeFolios ships but has no dedicated folios-merge.spec.ts. The phase-B spec file mentions it but case enumeration is unclear. For a P1 / 5-point story this is below the spec-coverage floor.

  • [SME-PROPOSED] Author folios-merge.service.spec.ts covering: (1) happy-path merge of two OPEN folios on same reservation; (2) reject merge across reservations unless groupMasterId matches; (3) reject merge if either folio is SETTLED/VOID/TRANSFERRED; (4) balance-recalc on target; (5) source folio status flips to TRANSFERRED with mergedIntoFolioId; (6) cross-tenant rejection; (7) idempotent — re-issuing the same merge with the same source/target returns 200 (or 409 with code=FOLIO_ALREADY_MERGED).

US-005B-02 — Folio Void & Credit Note​

Catalog status: not enumerated (under US-005-003 row) EPIC body status: Built (P1, 5 points, Sprint 3) Verified status: Built

Code locations:

  • hmp-frontdesk-service/src/folios/folios.service.ts:voidTransaction — marks FolioTransaction.isVoid=true, records voidReason/voidedBy/voidedAt, adjusts folio balance (reverses charge OR payment impact based on transactionType)
  • hmp-frontdesk-service/src/folios/folios.controller.ts — POST /folios/:id/void, POST /folios/void-transaction — manager-role guard via @Roles('SUPER_ADMIN','ADMIN','MANAGER')
  • hmp-frontdesk-service/src/folios/dto/folio.dto.ts:VoidTransactionDto — requires reason (min 10 chars per review file)
  • hmp-frontdesk-service/src/refunds/refund-ledger.service.ts + refund-ledger.controller.ts — POST /refund-ledger/credit-notes (manager+) + approval/issue lifecycle
  • hmp-frontdesk-service/src/einvoice/einvoice.service.ts:generateCreditNote — AP-T08-007 queue-routed credit-note IRN (CRN documentType) — see US-005-11 above

Unit + integration tests:

  • hmp-frontdesk-service/src/folios/folios.service.spec.ts — voidTransaction cases (success, reject already-voided, reject closed folio, balance reversal)
  • hmp-frontdesk-service/src/refunds/refund-ledger.service.spec.ts — credit-note CRUD + approval + IRN linkage
  • hmp-frontdesk-service/src/einvoice/einvoice.service.spec.ts — generateCreditNote queue-routing + idempotency

Flow tests: none direct; covered indirectly via flow-8 + check-out-einvoice flows

Real-backend smokes: none specific

Use cases referencing this story: none

SME notes:

  • Void writes the audit row but does NOT delete — this is the right call (auditable). RoomStatusLog-style audit trail per EP-002 D-006 should be the model.
  • Credit-note → CRN IRN → CRN.creditNoteIrn linkage (AP-T08-007) closes the GST loop. The legacy 2026-03-07 review file ("NOT_IMPL") is stale on this — same drift class as D-007.
  • Manager-role check is per-route via @Roles, not at the entity level — a non-manager calling POST /folios/:id/void gets 403 from RolesGuard. Story body §3.1 says "void with manager role"; AC is implicit.

Drift findings: (none net new — feeds D-007 stale-review class)

  • [SME-PROPOSED] Add AC: "POST /folios/:id/void SHALL be gated by @Roles('SUPER_ADMIN','ADMIN','MANAGER'); non-manager roles return 403 with code=ROLE_INSUFFICIENT. Audit row SHALL capture voidedBy=req.user.id + voidedAt=now() + reason (min 10 chars)."
  • [SME-PROPOSED] Add AC: "Voiding a CHARGE that has already been billed via e-invoice IRN SHALL automatically trigger a credit-note (CRN) IRN generation via EInvoiceService.generateCreditNote, queue-routed per AP-T08-007. The CRN.creditNoteIrn FK SHALL be populated on the original IRN's CreditNote row."

US-005B-03 — Folio Lock (Manager Approval)​

Catalog status: not enumerated EPIC body status: Partial (P2, 3 points, Sprint 4) Verified status: Partial — POST /folios/:id/lock route per EP-005B EPIC §4.1; manager-role check is the documented gap

Code locations:

  • hmp-frontdesk-service/src/folios/folios.controller.ts — POST /folios/:id/lock (per EPIC §4.1 table, "Lock folio (no further charges); requires manager role")
  • hmp-frontdesk-service/src/folios/folios.service.ts:lockFolio — [CITED-BUT-NOT-FOUND] — verify method exists; if not, this is a stub-shipped-as-Built case
  • hmp-frontdesk-service/src/folio-lock/folio-lock.service.ts — Redis distributed lock (NOT the same as the entity-level lock — this is concurrency)

Unit + integration tests: none dedicated — folio-lock.service.spec.ts covers Redis lock semantics only

Flow tests: none

Real-backend smokes: none

Use cases referencing this story: none

SME notes:

  • Terminology conflict — FolioLockModule is Redis distributed lock for CONCURRENCY (prevents two charges posting simultaneously). The story US-005B-03 is ENTITY lock (block all new charges after a manager freezes the folio). They are different concepts and easily confused.
  • Today's Folio.status enum is OPEN | SETTLED | TRANSFERRED | VOID — no LOCKED state. A locked folio cannot use SETTLED (still has balance) or VOID (still real). Either add a LOCKED status enum OR a lockedAt: DateTime? + lockedBy: String? column with a service-layer guard.
  • EPIC gap analysis row #5 calls this out: "Folio lock requires manager RBAC check (role check missing)".

Drift findings: (none new — feeds D-007 numbering + Partial-status class)

  • [SME-PROPOSED] Add AC: "POST /folios/:id/lock SHALL be gated by @Roles('SUPER_ADMIN','ADMIN','MANAGER'). Folio model SHALL gain lockedAt: DateTime? + lockedBy: String? + lockReason: String? columns. postCharge, postPayment, postAdjustment, splitFolio, transferCharges, mergeFolios SHALL all fail-fast (409, code=FOLIO_LOCKED) when lockedAt IS NOT NULL. Only voidTransaction SHALL be allowed on a locked folio (with manager role)."
  • [SME-PROPOSED] Add AC: "POST /folios/:id/unlock SHALL reverse the lock (manager-only, audit row written)."

US-005B-04 — Company / City Ledger​

Catalog status: not enumerated EPIC body status: Not Started (P1, 8 points, Sprint 4) Verified status: Partial — CompanyLedgerController + CompanyLedgerService exist with GET /companies, GET /companies/:id, POST /companies; gsa-181-182 spec exists. Aging buckets + monthly PDF statement gaps confirmed.

Code locations:

  • hmp-frontdesk-service/src/company-ledger/company-ledger.controller.ts — GET /companies, GET /companies/:id, POST /companies, more endpoints in the file
  • hmp-frontdesk-service/src/company-ledger/company-ledger.service.ts — company CRUD + ledger post-to-company
  • hmp-frontdesk-service/src/company-ledger/company-ledger.module.ts
  • Company cross-service read model in hmp-frontdesk-service/prisma/schema.prisma (per frontdesk CLAUDE.md "Cross-service read models")
  • hmp-frontdesk-service/src/folios/dto/folio.dto.ts:CompanyPaymentDto, SettleCompanyDto

Unit + integration tests:

  • hmp-frontdesk-service/src/company-ledger/company-ledger.service.spec.ts — service-level unit tests
  • hmp-frontdesk-service/src/company-ledger/__tests__/gsa-181-182-company-flow.spec.ts — gold-standard audit company-flow regression

Flow tests: none

Real-backend smokes: none specific

E2E (Playwright): none (no /billing/company-ledger page in dashboard)

Use cases referencing this story: none

SME notes:

  • EPIC §4.1 status table marks all 3 company-ledger endpoints as Not Started (GET /company-ledger, POST /company-ledger/:id/post, GET /company-ledger/:id/statement). The actual code has /companies CRUD shipped (different route) — this is a route-vs-table mismatch + reality drift. The folio-side POST /folios/:id/post-to-company (move folio balance to company AR on checkout) is unclear from current code.
  • Aging buckets (current / 30d / 60d / 90d+), credit-limit enforcement, monthly PDF statement, @IsGSTIN() on POST /companies validation — all per EPIC §4.1 — are gaps.
  • Frontend (/billing/company-ledger) is Not Started per EPIC §4.2 — confirmed: no hmp-web-dashboard/src/app/(dashboard)/billing/company-ledger/ directory.

Drift findings: (feeds D-001 numbering scheme + EPIC-vs-code drift)

  • [SME-PROPOSED] Update EPIC §4.1 status table — /companies CRUD is Built; ledger-side post/aging/statement are still Not Started. Don't conflate the two.
  • [SME-PROPOSED] Add AC: "POST /company-ledger/:companyId/post SHALL accept {folioId, amount} and move the folio balance to a CompanyLedgerEntry row ({tenantId, propertyId, companyId, folioId, amount, status='OUTSTANDING', invoiceNumber, postedAt, postedBy}). The source folio SHALL be settled (status=SETTLED) with a LEDGER_POST payment-equivalent row referencing the ledger entry. Atomic in $transaction."
  • [SME-PROPOSED] Add AC: "GET /company-ledger/:companyId/aging SHALL return buckets current / 30d / 60d / 90d+ / 120d+ computed against asOfDate (default today, IST). Each bucket carries count + totalOutstanding. Per-entry detail available via ?expand=entries."
  • [SME-PROPOSED] Add AC: "POST /companies SHALL validate gstin via @IsGSTIN() from @hmp/common (15-char format ^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$ — same as EP-002 D-003). Address state-code (first 2 digits) SHALL be cross-validated against the company's address state."
  • [SME-PROPOSED] Add AC: "Credit-limit enforcement (per EPIC §7 AC scenario): if outstanding + newPostAmount > creditLimit, return 409 with code=COMPANY_CREDIT_LIMIT_EXCEEDED + { outstanding, newPostAmount, creditLimit, exceededBy } body. Override available via ?managerOverride=true (audit row + Finance Manager role required)."

US-005B-05 — Refund Management​

Catalog status: not enumerated EPIC body status: Partial (P1, 5 points, Sprint 4) Verified status: Partial — Razorpay refund via payment-service + folio negative-payment row + RefundModal.tsx UI shipped; cash-refund petty-cash ledger + manager approval workflow gap confirmed

Code locations:

  • hmp-frontdesk-service/src/refunds/refund-ledger.service.ts + refund-ledger.controller.ts — credit-note CRUD + approval lifecycle (POST /refund-ledger/credit-notes, POST /refund-ledger/credit-notes/:id/approve)
  • hmp-frontdesk-service/src/payments/ — internal payment-record CRUD
  • hmp-frontdesk-service/src/folios/folios.service.ts:postPayment — accepts negative amount (refund); skips overpayment check; emits transactionCode='REFUND' (P0-1/P0-3)
  • hmp-payment-service/ — Razorpay refund API call (workspace cross-service)
  • hmp-frontdesk-service/src/petty-cash/ — petty-cash CRUD (for cash refund destination)
  • hmp-web-dashboard/src/app/(dashboard)/front-desk/folio/[id]/_components/RefundModal.tsx — refund UI (Razorpay + cash modes per RefundModal shape — verify)

Unit + integration tests:

  • hmp-frontdesk-service/src/refunds/refund-ledger.service.spec.ts — credit-note + approval
  • hmp-frontdesk-service/src/folios/folios.service.spec.ts — postPayment negative-amount (P0-1/P0-3)

Flow tests:

  • hmp-infrastructure/testing/flow-tests/src/flows/terminal-folio-settlement.flow.spec.ts (Scenario 4: "after happy capture, refund → Refund row with terminalDeviceId") — TBD
  • hmp-infrastructure/testing/flow-tests/src/flows/payments.flow.spec.ts (Scenario 7: "partial refund → full refund → over-refund is rejected") — TBD

Real-backend smokes: none specific

Use cases referencing this story: uc-refund-management.yaml [CITED-BUT-NOT-FOUND]

SME notes:

  • DPDPA art. 8(3) requires "reasonable period" refunds (industry interpretation: 7 business days). EP-002 D-010 already proposed Cancellation.refundSlaHours (default 168 = 7 days). The same SLA must apply to ad-hoc refunds (overpayment, dispute, goodwill).
  • Cash refund flow is the documented gap: needs PettyCashDisbursement row creation tied to the folio + manager approval workflow + receipt-print path. Today: postPayment accepts negative-amount (P0-1 contract) but no automated PettyCashDisbursement linkage.
  • Razorpay refund is in payment-service; the frontdesk path emits the negative-payment row on the folio after payment.captured.refund event (or similar) — verify the producer-side wiring. CLAUDE.md Gotcha 27 covers payment.captured for original capture; the refund-side equivalent is unclear.
  • Over-refund prevention — Scenario 7 of payments.flow.spec.ts ("over-refund is rejected") — confirm the service-layer check (cannot refund more than totalPayments - alreadyRefunded).

Drift findings: (none net new — feeds D-002 use-case-missing + D-007 stale-review)

  • [SME-PROPOSED] Add AC: "Cash refund SHALL create a PettyCashDisbursement row with {voucherNumber, category='REFUND', folioId, amount, status='AUTHORIZED' (manager+) or 'PENDING' (staff), receiptUrl?, custodian, createdBy} AND a folio FolioTransaction(transactionType='PAYMENT', amount=-X, transactionCode='REFUND_CASH') — both inside one $transaction."
  • [SME-PROPOSED] Add AC: "Refund SLA: from the moment a manager approves a refund, the system SHALL emit refund.processed within 7 business days (Cancellation.refundSlaHours default 168). Missed SLA emits refund.sla.breached for ops alerting (DPDPA art. 8(3) — see EP-002 D-010)."
  • [SME-PROPOSED] Add AC: "Over-refund SHALL return 409 with code=REFUND_EXCEEDS_PAYMENTS and body {folioId, totalPayments, alreadyRefunded, requestedRefund, maxAllowedRefund}."
  • [SME-PROPOSED] Author uc-refund-management.yaml covering: (1) Razorpay card refund full; (2) Razorpay card refund partial; (3) cash refund with petty-cash linkage; (4) over-refund rejected; (5) refund SLA breach alerting.

US-005B-06 — UPI QR Payment & Auto-Reconcile​

Catalog status: not enumerated EPIC body status: Partial (P1, 5 points, Sprint 4) Verified status: Partial — UPI QR generation per EPIC §4.1 and upi-qr.flow.spec.ts (4 scenarios); webhook auto-reconcile claimed in CLAUDE.md Gotcha 27 (qr_code.credited webhook → status PAID) but EPIC §4.1 lists as Partial

Code locations:

  • hmp-frontdesk-service/src/folios/folios.controller.ts — POST /folios/:id/upi-qr (per EPIC §4.1)
  • hmp-frontdesk-service/src/folios/folios.service.ts:generateUpiQr [CITED-BUT-NOT-FOUND] — verify
  • hmp-payment-service/src/upi-qr/ [CITED-BUT-NOT-FOUND] — Razorpay qr_code.credited webhook handling (cross-service to payment-service)
  • hmp-frontdesk-service/src/folios/consumers/payment-captured.consumer.ts — payment.captured consumer handles providerCode='razorpay-upi-qr' → PaymentMode UPI mapping (CLAUDE.md Gotcha 27)

Unit + integration tests:

  • hmp-frontdesk-service/src/folios/consumers/payment-captured.consumer.spec.ts — 20 cases, providerCode mapping incl. UPI

Flow tests:

  • hmp-infrastructure/testing/flow-tests/src/flows/upi-qr.flow.spec.ts (4 scenarios — create QR ACTIVE, poll ACTIVE, webhook credited → PAID, manual close → CLOSED) — TBD

Real-backend smokes: none specific

Use cases referencing this story: none

SME notes:

  • Auto-reconcile path is two-hop: Razorpay → payment-service qr_code.credited webhook → payment.captured outbox emit → PaymentCapturedConsumer (frontdesk) → folio negative-payment row. CLAUDE.md Gotcha 27 documents the consumer side. EPIC §4.1 status "QR generation works; auto-reconciliation webhook pending" is stale post-Task #11 (2026-05-19) — the consumer wire-up makes auto-reconcile work end-to-end.
  • QR expiry — Razorpay QRs have a configurable expiry (default 1 day for static; per-folio QRs typically expire when folio settles). Story body doesn't specify expiry policy.
  • Per-folio QR dedup — if guest scans the QR twice, idempotency relies on Razorpay's payment_id being unique → PaymentCapturedConsumer dedup key folio:{folioId}:payment-captured:{paymentId}. Verify in spec.
  • UPI VPA capture for refund — auto-refund to original VPA requires capturing the payer VPA on credit webhook. CLAUDE.md is silent.

Drift findings: (none new — feeds D-007 stale-review class on auto-reconcile status)

  • [SME-PROPOSED] Update EPIC §4.1 status for "auto-reconciliation webhook" from Partial to Built (per CLAUDE.md Gotcha 27, Task #11 2026-05-19 wire-up). The producer-side webhook and consumer-side wiring both exist.
  • [SME-PROPOSED] Add AC: "UPI QR expiresAt SHALL default to folio.checkOutDate + 24h (or now + 7d for unattached folios). On folio settle, an ACTIVE QR SHALL be force-CLOSED to prevent post-settle credit."
  • [SME-PROPOSED] Add AC: "On qr_code.credited webhook, the payer VPA SHALL be persisted to Payment.payerUpiId for future refund routing."

US-005B-07 — Minibar & Room Service Charge Post​

Catalog status: not enumerated EPIC body status: Built (P1, 3 points, Sprint 3) Verified status: Built — POS → folio path via PosFolioChargeConsumer (29 specs incl. AP-T07-004 GST derivation)

Code locations:

  • hmp-pos-service/ — POS owns the originating order + KOT
  • hmp-frontdesk-service/src/folios/consumers/pos-folio-charge.consumer.ts — subscribes to kot.completed / order.closed / pos.charge.posted-class events; AP-T07-002 reads tenant from folio.reservation.property.tenantId (not event.tenantId); AP-T07-004 derives GST split from customerState
  • hmp-frontdesk-service/src/folios/folios.service.ts:postCharge — handles transactionCode='MINIBAR' / 'ROOM_SERVICE' / 'LAUNDRY' / 'TELEPHONE'

Unit + integration tests:

  • hmp-frontdesk-service/src/folios/consumers/pos-folio-charge.consumer.spec.ts — 29 cases (covered above under US-005-03)
  • hmp-frontdesk-service/src/folios/folios.service.spec.ts — postCharge with quantity multiplier

Flow tests:

  • hmp-infrastructure/testing/flow-tests/src/flows/flow-7-pos-to-folio.spec.ts (6 scenarios — outlet/menu/order/post-as-room-charge/folio-CHARGE-match) — 0/10 PASS at snapshot
  • hmp-infrastructure/testing/flow-tests/src/flows/pos-folio-charge.flow.spec.ts (6 scenarios) — TBD

Real-backend smokes: none specific

Use cases referencing this story: none

SME notes:

  • POS → folio is the most-failing flow on the platform at the 2026-05-16 snapshot (flow-7 was 0/10 PASS). The root cause was the W2.A.1 schema-only drift (AP-T07-004 finally added the DB columns 2026-05-23). Re-measure post-shipping fix.
  • POS-1 (memory: "tenant needs PROFESSIONAL/ENTERPRISE plan") is a CONFIG bug not a code bug — entry in 2026-05-21-qa-remediation-shipped notes — operator must PATCH /feature-flags/plan on the tenant. Cross-reference here for completeness.
  • Spa charges follow the same producer pattern via SpaFolioChargeConsumer (cross-service) — same idempotency + tenant-defense.

Drift findings: (none new — feeds D-007 stale-review for flow-7 pass rate)

  • [SME-PROPOSED] Add AC: "POS, Spa, Banquet, In-Room-Dining producers SHALL all emit pos.charge.posted (or topic-equivalent) with the same payload shape: {tenantId, propertyId, folioId, transactionCode, amount, taxAmount, cgstAmount?, sgstAmount?, igstAmount?, customerState?, placeOfSupply?, quantity, description, postedBy, originatingService, originatingOrderId}. Each consumer SHALL verify folio.reservation.property.tenantId === event.tenantId (NOT trust the event tenantId — AP-T07-002 lesson)."

US-005B-08 — Folio PDF Export & Email​

Catalog status: not enumerated EPIC body status: Built (P1, 3 points, Sprint 3) Verified status: Built-Untested — PDF service has 1 spec aligned post-Top-10-closure; email-dispatch handler coverage unclear

Code locations:

  • hmp-frontdesk-service/src/folios/invoice-pdf.service.ts — pdfkit-based GST-compliant invoice PDF; reads folio.irn directly (AP-T08-006); renders DRAFT watermark when null
  • hmp-frontdesk-service/src/folios/folios.controller.ts — GET /folios/:id/invoice/pdf (@Res() res: Response + res.end(buffer) pattern)
  • Email-dispatch path — likely via @hmp/messaging event → notification-service consumer → SES (per memory: "Email migrated SendGrid→AWS SES" 2026-05-21)
  • AP-CC-5 email-invoice-delivery.flow.spec.ts — covers the EMAIL NotificationLog row creation within 5s of reservation create (not folio close — see drift)

Unit + integration tests:

  • hmp-frontdesk-service/src/folios/invoice-pdf.service.spec.ts — re-aligned post-Top-10 closure (canonical: FoliosService.getNextInvoiceNumber is the auto-numbering owner, NOT this service)

Flow tests:

  • hmp-infrastructure/testing/flow-tests/src/flows/email-invoice-delivery.flow.spec.ts (4 scenarios — folio create, EMAIL NotificationLog within 5s of reservation create, sender metadata, notification-service health) — TBD

Real-backend smokes: none specific

Use cases referencing this story: none

SME notes:

  • PDF SHOULD render the IRN signed-QR-code visually (per einvoice CLAUDE.md flow §AP-T08-006). The PDF service has the data (folio.irn, folio.signedQRCode) — verify rendering.
  • The email-invoice flow trigger is reservation-create, not folio-close — that's a smell. Guest typically wants the invoice at checkout (post-settle), not at booking. AP-CC-5 flow asserts the wrong trigger.
  • B2B invoice email should CC the company's billing contact, not just the guest. Today's email path likely uses reservation.guest.email only.
  • Email retry + DLQ — notification-service has its own retry queue; verify the folio-invoice event survives a notification-service outage.

Drift findings: (none new — feeds D-002 use-case-missing class)

  • [SME-PROPOSED] Add AC: "Invoice PDF SHALL render the IRN as text + visual QR code (Base64-decoded from folio.signedQRCode) when folio.irn IS NOT NULL. Watermark 'DRAFT — IRN PENDING' when null."
  • [SME-PROPOSED] Add AC: "Email dispatch SHALL trigger on folio.closed event (NOT reservation.created). For B2B (company-linked) folios, email SHALL CC the company billing-contact in addition to the primary guest. AP-CC-5 flow-test SHALL be retargeted to assert NotificationLog within 5s of folio.closed."

See also​