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) usesUS-005-001..022(front-desk umbrella) with three folio rows (US-005-003Folio Mgmt,US-005-007E-Invoice,US-005-017Folio 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(perreviews/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 hitFolioTransaction, but the story owner is EP-005C.
Status legend
| Status | Meaning |
|---|---|
Built | Code exists + tests exist + tests pass |
Built-Untested | Code exists, no automated test coverage (or test exists but is a stub) |
Partial | Code exists for some acceptance criteria, gaps for others |
Stub | Code is a placeholder / TODO / returns mock data |
Not Started | No code |
Summary
| Metric | Count |
|---|---|
| Total stories (EPIC body) | 13 |
| Catalog claims "Built" | 9 |
| Catalog claims "Partial" | 3 |
| Catalog claims "Not Started" | 1 |
| Verified-Built | 7 (US-005-03, -04, -05, -06, -11, US-005B-02, US-005B-07) |
| Built-Untested | 1 (US-005B-08 — folio PDF + email; PDF service has 1 spec, email-dispatch handler has 0) |
| Partial | 4 (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) |
| Stub | 0 |
| Not Started | 1 (US-005B-04 company/city ledger — entity + endpoints partial; statement + aging gaps) |
| Drift findings | 9 (D-001..D-009; 4 P0 / 4 P1 / 1 P2) |
| [SME-PROPOSED] refinements | 22 |
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/mergehmp-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),getDailySummaryhmp-frontdesk-service/src/folios/dto/folio.dto.ts—PostChargeDto(carries optionalcgstAmount/sgstAmount/igstAmount/customerState/placeOfSupply— W2.A.1),PostPaymentDto,TransferChargeDto,VoidTransactionDto,CreateFolioDto,AdjustmentDto,TdsAdjustmentDto,SplitFolioDto,MultiSplitFolioDto,MergeFoliosDtohmp-frontdesk-service/src/folios/tax-split.util.ts— intra/inter-state split helperhmp-frontdesk-service/src/folios/consumers/pos-folio-charge.consumer.ts— POSkot.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-servicepayment.captured→ folio payment (Task #11 wire-up, 2026-05-19)hmp-frontdesk-service/src/folios/consumers/folio-charge-posting.consumer.ts— internal outbox publisherhmp-frontdesk-service/src/folio-lock/folio-lock.service.ts— Redis distributed lock to prevent concurrent post on the same foliohmp-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, postAdjustmenthmp-frontdesk-service/src/folios/folios-phase-b.service.spec.ts— phase B operationshmp-frontdesk-service/src/folios/folios-split.service.spec.ts— split + multi-splithmp-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 master32addb1-class commit chain; verify againstnpm testin branchhmp-frontdesk-service/src/folios/__tests__/gsa-006-invoice-ownership.spec.ts— invoice-number ownership scopehmp-frontdesk-service/src/folios/__tests__/gsa-067-igst-split.spec.ts— intra-vs-inter state GST splithmp-frontdesk-service/src/folios/consumers/pos-folio-charge.consumer.spec.ts— 29 cases incl. AP-T07-002 tenant defense + AP-T07-004 GST derivationhmp-frontdesk-service/src/folios/consumers/payment-captured.consumer.spec.ts— 20 cases for the Razorpay capture → folio payment pathhmp-frontdesk-service/src/folios/consumers/spa-folio-charge.consumer.spec.ts— spa cross-servicehmp-frontdesk-service/src/folios/consumers/folio-charge-posting.consumer.spec.ts— outbox-style internal consumerhmp-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 idempotencyhmp-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 loadshmp-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 pagehmp-web-dashboard/e2e/tests/billing-overview.spec.tshmp-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) anduc-tds-adjustment(P0-11) cited in frontdesk CLAUDE.md but not as YAML rows inuse-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 memoryproject_2026-05-20-top-10-tracker-closure.mdafter 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 atinvoice-pdf.service.spec.tswas 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:postChargenow passesdto.cgstAmount/sgstAmount/igstAmount/customerState/placeOfSupplystraight through; legacy callers fall through to schemadefault(0). Migration20260523120000_add_folio_transaction_gst_splitfinally adds the columns to the DB (was schema-only drift for ~2 weeks). - Tax-exempt + RCM (P0-5/P0-6) force
taxAmount=0even 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.codeslug regex^[A-Z0-9]{2,8}$resolves the prefix; falls back to 6-char hash of propertyId. Replaces the hard-codedHMP/prefix that the old review file still references. - FU-55 dual-emit —
postChargenow emits BOTH legacyfolio.charge.postedAND canonicalfolio.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 formatYYYY-YYare 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-NNscheme as canonical for sub-EPIC stories, and rewrite the catalog's EP-005 row forUS-005-003to readUS-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 isBuilt; the underlying US-005B-04 company-ledger isNot 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.yamlcovering: (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.yamlcovering: cash/card/UPI/NEFT, foreign-currency conversion via CurrencyClientService, idempotency viapaymentRef, 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/tdswith 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 structuredreferencepayload and decrements folio balance accordingly. Audit trail:postedBypopulated 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 updatehmp-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-servicepayment.capturedRedis Streams topic, resolves folio viaexternalReferenceType=FOLIO|RESERVATION, posts payment viaFoliosService.postPayment(dto, 'system:payment-captured-consumer', ...). Dedup viaFolioTransaction.referenceshapefolio:{folioId}:payment-captured:{paymentId}. Tenant-mismatch defense onfolio.reservation.property.tenantId.hmp-frontdesk-service/src/payments/— internal payment-record CRUDhmp-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—postPaymentcases incl. 0/NaN reject, negative refund (P0-1), overpayment trigger, multi-currencyhmp-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/paymentssettles 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) — TBDhmp-infrastructure/testing/flow-tests/src/flows/payments.flow.spec.ts— 8 scenarios for the payment-service surface; thepayment.capturedevent is the trigger forPaymentCapturedConsumer
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 withtransactionCode='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
PaymentCapturedConsumeris the ONLY supported path for booking-engine redirect captures, terminal-charge captures, payment-link captures, and UPI-QR captures (all four producer paths emitpayment.capturedvia 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-serviceFolioPaymentConsumeruses this as the Redis SETNX dedup key. Without it, falls back to afolioId:amount:balancecomposite key. Story doesn't pin the event-payload contract. paymentRefidempotency — the EPIC body §3.1 lists "Idempotent payment creation" as in-scope; the DTO haspaymentRef?: stringbut the service-layer enforcement is via the consumer'sreferencededup pattern, not a DB unique constraint onPayment.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/paymentsSHALL reject (409 withcode=PAYMENT_DUPLICATE_REF) any payment whosepaymentRefmatches an existing Payment row for the same folio + same payment-mode. The check SHALL be a unique partial indexPayment(folioId, paymentMode, paymentRef) WHERE paymentRef IS NOT NULLenforced by Prisma at write time." - [SME-PROPOSED] Add AC: "The
PaymentCapturedConsumerdedup key SHALL remainfolio:{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,multiSplitFoliohmp-frontdesk-service/src/folios/folios.controller.ts— POST/folios/transfer, POST/folios/:id/split, POST/folios/:id/multi-splithmp-frontdesk-service/src/folios/dto/folio.dto.ts—TransferChargeDto,TransferChargesDto,SplitFolioDto,MultiSplitFolioDtohmp-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 coveragehmp-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
transferRuledriven bychargeType(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 withtransactionCode 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/splitSHALL accept a rule-basedsplitRules: SplitRule[]body shape where each rule selects transactions bychargeTypes[],transactionCodes[],amountFilter? {min,max}, orexplicit 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,currencyand incrementedfolioNumber({folioNumber}-S{n}shape). Audit trail: source folio receives aSPLIT_OUTaudit 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— validatesbalance===0, setsstatus=SETTLED + settledAt=now(), publishes BOTH legacyfolio.settledAND canonicalfolio.closed(FU-55 dual-emit 2026-05-23)hmp-frontdesk-service/src/folios/folios.controller.ts:POST /folios/:id/settlehmp-frontdesk-service/src/checkout/checkout.service.ts— settlement orchestration inside checkout pipeline (callssettleFolioper open folio)
Unit + integration tests:
hmp-frontdesk-service/src/folios/folios.service.spec.ts—settleFoliocases: zero-balance success, reject with balance, reject already-settled, reject voided foliohmp-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-checkouthmp-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/settlecloses 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 snapshothmp-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.settledand canonicalfolio.closedare published (canonical addssegment,countryCode, remapstotalAmount/closedAt/taxAmount). Until Track 4 sweep, both must publish; consumers should subscribe to the canonical event only. - Settle-during-checkout —
checkout.service.tscallssettleFolioper OPEN folio; FINDING-008 fix means each settled folio surfaces its IRN in the checkout response (verified incheck-out-einvoice.flow.spec.ts). If a tenant is a demo tenant, the per-folio IRN isSKIPPED_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
ROUNDINGadjustment 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 aROUNDINGadjustment row absorbing any residual|balance| <= 1.00 INR(configurable per property:Property.settlementRoundingTolerance, default 1.00). The adjustment carriesreference={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/:irnhmp-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 inNODE_ENV=production + COMPLIANCE_MODE=livewithout any ofEINVOICE_CLIENT_ID/SECRET/USERNAME/PASSWORD/GSTIN(escape hatch:EINVOICE_ALLOW_MOCK_IN_PROD=true) — AP-T08-001 / 2026-05-19hmp-frontdesk-service/src/einvoice/nic-einvoice.adapter.ts—/eivital/v1.04/auth, token cached until expiry, sandbox vs production URL resolutionhmp-frontdesk-service/src/einvoice/mock-einvoice.adapter.ts— SHA-256 64-char hex IRNs, structurally valid 15-digit ack numbers, Base64 QR payloadhmp-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 toFolio.irn/signedQRCode/ackDate/ackNumber(AP-T08-006); CRN jobs write toCreditNote.creditNoteIrn + status=IRN_GENERATEDand MUST NOT touch Folio.irnhmp-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 callbackhmp-frontdesk-service/src/einvoice/eway-bill.service.ts— related GST compliancehmp-frontdesk-service/src/demo-tenant/—DemoTenantGuardService(Plan 4 2026-05-13): whenTenant.isDemoTenant=true, NIC submission is SKIPPED (no job row, no API call, structuredDEMO_TENANT_EINVOICE_SKIPPEDlog) — stricter thanEINVOICE_MOCKhmp-frontdesk-service/src/folios/invoice-pdf.service.ts— readsfolio.irndirectly (AP-T08-006); renders "DRAFT — IRN PENDING" watermark when null; renders real IRN + QR when sethmp-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 onlyNIC_EINVOICE_CLIENT_IDbut code now requires all 5EINVOICE_*vars) — fixed in2026-05-19-gold-standard-pass-shippedsession (memory)hmp-frontdesk-service/src/einvoice/einvoice-job.processor.spec.ts— INV-vs-CRN branch + retry + DLQhmp-frontdesk-service/src/einvoice/nic-einvoice.adapter.spec.ts—[CITED-BUT-NOT-FOUND]historically described as 2-trivial-tests; verify post-AP-T08-003 updatehmp-frontdesk-service/src/einvoice/mock-einvoice.adapter.spec.ts— mock IRN format + payload shapehmp-frontdesk-service/src/folios/invoice-pdf.service.spec.ts— re-aligned aftergetNextInvoiceNumberownership 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/generateis reachable", "...response contains a jobId and QUEUED status", "GET/api/einvoice/jobs/:jobIdeventually reaches COMPLETED with a 64-char IRN", "completed job carries GST amounts", "POST/api/einvoice/generatea second time for the same folio returns the same IRN") — 0/15 PASS at snapshothmp-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)") — TBDhmp-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) — TBDhmp-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 (
996311room,996331F&B — einvoice CLAUDE.md), B2B/B2C branching (folio.company?.gstintriggers B2B viacompanyinclude — 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 + NICsignedQRCode), PDF (InvoicePdfServicereadsfolio.irndirectly — 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: truefrom thegenerateIrnForFoliofolio query, every corporate booking silently regresses to B2C withbuyer.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 byEInvoiceJobProcessoron COMPLETED. PDF reads from Folio, not EInvoiceJob — backfill script atprisma/migrations-data/backfill-folio-irn.sqlfor legacy rows. - Compliance report (P0-16) is the canonical daily-reconciliation dashboard — buckets folios by
filed / queued / processing / deadLetter / missingfor 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. PromoteEInvoiceJob.statusenum to addSKIPPED_DEMO_TENANT(with migration) so compliance evidence is queryable in SQL, not greppable in logs. - [SME-PROPOSED] Add AC: "When
Tenant.isDemoTenant=true,generateIrnForFolioSHALL persist anEInvoiceJobrow withstatus='SKIPPED_DEMO_TENANT'(NO NIC API call, NO IRN), so the daily compliance report can include ademoSkippedbucket 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
getComplianceReportdaily run; DEAD_LETTER jobs SHALL be reviewed within 1 business day withPOST /einvoice/dead-letter/:jobId/retry." - [SME-PROPOSED] Author
uc-einvoice-generation-async.yamlcovering: 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/mergehmp-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—MergeFoliosDtohmp-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 +
mergedIntoFolioIdpopulated; (c) source folio transactions are NOT physically moved (audit trail) — instead, mirrored TRANSFER-out + TRANSFER-in pair; (d)totalCharges+totalPayments+totalTaxrecalculated 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.tscovering: (1) happy-path merge of two OPEN folios on same reservation; (2) reject merge across reservations unlessgroupMasterIdmatches; (3) reject merge if either folio is SETTLED/VOID/TRANSFERRED; (4) balance-recalc on target; (5) source folio status flips to TRANSFERRED withmergedIntoFolioId; (6) cross-tenant rejection; (7) idempotent — re-issuing the same merge with the same source/target returns 200 (or 409 withcode=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— marksFolioTransaction.isVoid=true, recordsvoidReason/voidedBy/voidedAt, adjusts folio balance (reverses charge OR payment impact based ontransactionType)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— requiresreason(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 lifecyclehmp-frontdesk-service/src/einvoice/einvoice.service.ts:generateCreditNote— AP-T08-007 queue-routed credit-note IRN (CRNdocumentType) — 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 linkagehmp-frontdesk-service/src/einvoice/einvoice.service.spec.ts—generateCreditNotequeue-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 callingPOST /folios/:id/voidgets 403 fromRolesGuard. 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/voidSHALL be gated by@Roles('SUPER_ADMIN','ADMIN','MANAGER'); non-manager roles return 403 withcode=ROLE_INSUFFICIENT. Audit row SHALL capturevoidedBy=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 casehmp-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 —
FolioLockModuleis Redis distributed lock for CONCURRENCY (prevents two charges posting simultaneously). The storyUS-005B-03is ENTITY lock (block all new charges after a manager freezes the folio). They are different concepts and easily confused. - Today's
Folio.statusenum isOPEN | SETTLED | TRANSFERRED | VOID— noLOCKEDstate. A locked folio cannot useSETTLED(still has balance) orVOID(still real). Either add aLOCKEDstatus enum OR alockedAt: 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/lockSHALL be gated by@Roles('SUPER_ADMIN','ADMIN','MANAGER'). Folio model SHALL gainlockedAt: DateTime?+lockedBy: String?+lockReason: String?columns.postCharge,postPayment,postAdjustment,splitFolio,transferCharges,mergeFoliosSHALL all fail-fast (409,code=FOLIO_LOCKED) whenlockedAt IS NOT NULL. OnlyvoidTransactionSHALL be allowed on a locked folio (with manager role)." - [SME-PROPOSED] Add AC: "
POST /folios/:id/unlockSHALL 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 filehmp-frontdesk-service/src/company-ledger/company-ledger.service.ts— company CRUD + ledger post-to-companyhmp-frontdesk-service/src/company-ledger/company-ledger.module.tsCompanycross-service read model inhmp-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 testshmp-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/companiesCRUD shipped (different route) — this is a route-vs-table mismatch + reality drift. The folio-sidePOST /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()onPOST /companiesvalidation — all per EPIC §4.1 — are gaps. - Frontend (
/billing/company-ledger) isNot Startedper EPIC §4.2 — confirmed: nohmp-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 —
/companiesCRUD isBuilt; ledger-sidepost/aging/statementare stillNot Started. Don't conflate the two. - [SME-PROPOSED] Add AC: "
POST /company-ledger/:companyId/postSHALL accept{folioId, amount}and move the folio balance to aCompanyLedgerEntryrow ({tenantId, propertyId, companyId, folioId, amount, status='OUTSTANDING', invoiceNumber, postedAt, postedBy}). The source folio SHALL be settled (status=SETTLED) with aLEDGER_POSTpayment-equivalent row referencing the ledger entry. Atomic in$transaction." - [SME-PROPOSED] Add AC: "
GET /company-ledger/:companyId/agingSHALL return bucketscurrent / 30d / 60d / 90d+ / 120d+computed againstasOfDate(default today, IST). Each bucket carriescount + totalOutstanding. Per-entry detail available via?expand=entries." - [SME-PROPOSED] Add AC: "
POST /companiesSHALL validategstinvia@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 withcode=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 CRUDhmp-frontdesk-service/src/folios/folios.service.ts:postPayment— accepts negative amount (refund); skips overpayment check; emitstransactionCode='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 perRefundModalshape — verify)
Unit + integration tests:
hmp-frontdesk-service/src/refunds/refund-ledger.service.spec.ts— credit-note + approvalhmp-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") — TBDhmp-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
PettyCashDisbursementrow creation tied to the folio + manager approval workflow + receipt-print path. Today:postPaymentaccepts negative-amount (P0-1 contract) but no automatedPettyCashDisbursementlinkage. - Razorpay refund is in
payment-service; the frontdesk path emits the negative-payment row on the folio afterpayment.captured.refundevent (or similar) — verify the producer-side wiring. CLAUDE.md Gotcha 27 coverspayment.capturedfor 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 thantotalPayments - 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
PettyCashDisbursementrow 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.processedwithin 7 business days (Cancellation.refundSlaHoursdefault 168). Missed SLA emitsrefund.sla.breachedfor ops alerting (DPDPA art. 8(3) — see EP-002 D-010)." - [SME-PROPOSED] Add AC: "Over-refund SHALL return 409 with
code=REFUND_EXCEEDS_PAYMENTSand body{folioId, totalPayments, alreadyRefunded, requestedRefund, maxAllowedRefund}." - [SME-PROPOSED] Author
uc-refund-management.yamlcovering: (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]— verifyhmp-payment-service/src/upi-qr/[CITED-BUT-NOT-FOUND]— Razorpayqr_code.creditedwebhook handling (cross-service to payment-service)hmp-frontdesk-service/src/folios/consumers/payment-captured.consumer.ts—payment.capturedconsumer handlesproviderCode='razorpay-upi-qr'→ PaymentModeUPImapping (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.creditedwebhook →payment.capturedoutbox 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_idbeing unique →PaymentCapturedConsumerdedup keyfolio:{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
PartialtoBuilt(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
expiresAtSHALL default tofolio.checkOutDate + 24h(ornow + 7dfor unattached folios). On folio settle, an ACTIVE QR SHALL be force-CLOSED to prevent post-settle credit." - [SME-PROPOSED] Add AC: "On
qr_code.creditedwebhook, the payer VPA SHALL be persisted toPayment.payerUpiIdfor 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 + KOThmp-frontdesk-service/src/folios/consumers/pos-folio-charge.consumer.ts— subscribes tokot.completed/order.closed/pos.charge.posted-class events; AP-T07-002 reads tenant fromfolio.reservation.property.tenantId(not event.tenantId); AP-T07-004 derives GST split fromcustomerStatehmp-frontdesk-service/src/folios/folios.service.ts:postCharge— handlestransactionCode='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 snapshothmp-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-shippednotes — operator mustPATCH /feature-flags/planon 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 verifyfolio.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; readsfolio.irndirectly (AP-T08-006); renders DRAFT watermark when nullhmp-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/messagingevent → 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.getNextInvoiceNumberis 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.emailonly. - 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) whenfolio.irn IS NOT NULL. Watermark 'DRAFT — IRN PENDING' when null." - [SME-PROPOSED] Add AC: "Email dispatch SHALL trigger on
folio.closedevent (NOTreservation.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 offolio.closed."
See also
README.md— folder indexRTM-DASHBOARD.md— platform-wide roll-upPLAYBOOK.md— RTM authoring patternEP-002-rtm.md— pilot template../epics/EP-005B-folios-billing.md— sub-EPIC body../epics/EP-005-front-desk-operations.md— parent EPIC../reviews/EP-005/US-005-003-review.md,-007-review.md— older review files (the -007 file is STALE per D-007 — re-run review)