BF-005 — Check-Out + GST E-Invoice (IRN)
Business flow (cross-EPIC user journey). Reconciled 2026-05-25 as Wave 2 of the RTM Reconciliation Scale-Out. Spans EP-005A (check-out controller), EP-005B (folio settlement + e-invoice queue), EP-005C (express auto-settle from night audit), and EP-012C (GSTR-1/3B reporting from the IRN register).
LAUNCH BLOCKER. Flow-8 (
flow-8-check-out.spec.ts, 15 scenarios) was 0/15 PASS at the 2026-05-16 baseline snapshot — the single worst result on the platform. Substantial fixes have shipped since (D1 reconciler 2026-05-22, FU-55 dual-emit 2026-05-23, AP-T07-004 GST split 2026-05-23, AP-T08-006 Folio.irn denorm 2026-04-29, AP-T08-007 credit-note queue 2026-05-23) but the snapshot has not been re-measured. Operator action tracked under D-005A-008.Each step names (a) the user story that owns the requirement, (b) the primary code path that runs it, (c) the closest automated test that exercises it, and (d) pre/post-conditions and notes. Where the spec contains no matching catalog story, the step is tagged
[SME-PROPOSED]as a NEW story for the SME's intake — these are the gaps the RTM pilot is designed to surface.
Personas
| Persona | Role | Source |
|---|---|---|
| FRONT_DESK_AGENT | Day-shift checkout staff | 01-epic-catalog.md (EP-005 row) |
| MANAGER / ACCOUNTS | Force-checkout / void / refund approver | EPIC body EP-005B §3.1 |
| Guest (B2C) | Indian resident, walks up at checkout | Aadhaar Act 2016 |
| Guest (B2B) | Corporate / GSTIN-bearing buyer | GST CGST/IGST Acts |
| Compliance Officer | Daily GSTR-1/3B reconciliation, IRN compliance report | EP-012C getComplianceReport (P0-16) |
| Night-audit batch | EP-005C K8s CronJob 02:00 IST — invokes express auto-settle | kubernetes/jobs/night-audit-cronjob.yaml |
Entry points
| Entry | URL | Backed by |
|---|---|---|
| Dashboard departures board | https://dashboard.<env>.keynova.ai/front-desk/departures | hmp-web-dashboard/src/app/(dashboard)/front-desk/departures/page.tsx |
| Dashboard checkout wizard | https://dashboard.<env>.keynova.ai/front-desk/check-out/[id] | hmp-web-dashboard/src/app/(dashboard)/front-desk/check-out/[id]/page.tsx |
| Dashboard folio page (pre-checkout review) | https://dashboard.<env>.keynova.ai/front-desk/folio/[id] | hmp-web-dashboard/src/app/(dashboard)/front-desk/folio/[id]/page.tsx |
| Express auto-settle (background) | n/a — night-audit CronJob 02:00 IST | kubernetes/jobs/night-audit-cronjob.yaml → POST /api/check-out/express (US-005A-03 — STUB today) |
The 12-step journey
Step 1: Pre-checkout folio review
- Story: US-005-03 (Folio Creation & Charges) — read path of the same store.
- Code:
hmp-frontdesk-service/src/folios/folios.controller.ts—GET /folios/:id,GET /reservations/:reservationId/folios,GET /folios/:id/invoice. Dashboard surface:hmp-web-dashboard/src/app/(dashboard)/front-desk/folio/[id]/page.tsx. Charge surfaces include cross-service consumers:pos-folio-charge.consumer.ts(POS room-charge / KOT / minibar),spa-folio-charge.consumer.ts(spa), late-charge service (frontdesk early/late-checkout fees). - Test:
folios.service.spec.ts(broad coverage ofgetById),folios.controller.spec.ts,pos-folio-charge.consumer.spec.ts(29 cases — most-failing flow-7 path),spa-folio-charge.consumer.spec.ts. Cross-tenant:folios/__tests__/folio-cross-tenant-denial.spec.ts(9 cases). - Pre-conditions: Reservation
status=CHECKED_IN. Foliostatus=OPEN. All upstream producers (POS, spa, banquet, in-room dining) have flushed theirpos.charge.posted-class events and thePosFolioChargeConsumerhas applied them. - Post-conditions: Folio surface lists every CHARGE / PAYMENT / ADJUSTMENT / TRANSFER row with GST split (CGST/SGST/IGST per AP-T07-001/004). Read-only — no mutation.
- Notes:
- Late-arriving charges are the dominant source of guest dispute at checkout.
PosFolioChargeConsumeris async — if KOT was posted within 5 minutes of checkout request, the folio page may show the line stale. UI must invalidate the React Query cache on the checkout-wizard mount (verify in dashboard code). - The folio response includes a
denormalizedIRN block per AP-T08-006 (folio.irn,signedQRCode,ackDate,ackNumber). If the previous checkout attempt half-fired the IRN queue, this block shows the in-flight state — the wizard should refuse to re-fire and show the existing IRN status card. - Flow-7 was 0/10 PASS at the 2026-05-16 snapshot — the W2.A.1 schema-only drift left
FolioTransaction.cgstAmount/sgstAmount/igstAmount/customerState/placeOfSupplycolumns missing from the DB. AP-T07-004 migration20260523120000_add_folio_transaction_gst_splitfinally added them on 2026-05-23. Re-measure post-fix.
- Late-arriving charges are the dominant source of guest dispute at checkout.
Step 2: B2B buyer GSTIN capture
- Story:
[SME-PROPOSED]new story —US-005-NEW-CHECKOUT-B2B-GSTIN"Capture / verify buyer GSTIN at checkout for B2B folios". Closest catalog story is US-005-002 (Standard Check-Out) which has the auth surface but not the AC. Implementation half-shipped: backend AP-T08-003 readsfolio.company.gstinfor B2B IRN; dashboard surface for ad-hoc GSTIN entry at checkout is the missing half (D-005A-009 P0). - Code:
hmp-frontdesk-service/src/einvoice/einvoice.service.ts:generateIrnForFolio— B2B include is load-bearing (CLAUDE.md Gotcha 6 / AP-T08-003): ifcompany: trueis dropped from the folio query, every corporate booking silently regresses to B2C withbuyer.GSTIN='URP', breaking GSTR-1/GSTR-2B reconciliation.Folio.buyerGstincolumn exists;PATCH /folios/:id/buyer-gstinroute is the proposed shim. - Test:
einvoice.service.spec.ts(B2B GSTIN path 5 specs — AP-T08-003). No spec for ad-hoc dashboard entry (the dashboard surface doesn't exist). - Pre-conditions: Step 1 completed. Folio is
folioType=COMPANYOR guest indicates B2B at checkout. Buyer GSTIN must match^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$. - Post-conditions:
Folio.buyerGstinpopulated. The first 2 digits of the buyer GSTIN drive intra-state vs inter-state for IGST resolution per AP-T07-004 — ifbuyerGstin[0:2] === property.gstin[0:2]→ CGST+SGST; else → IGST. Mismatch with existingcustomerStatereturns 400 with codeBUYER_GSTIN_STATE_MISMATCH. - Notes:
- D-005A-005 / D-005A-009 (both P0): the right place to capture buyer GSTIN is at check-in (so the registration card carries it). Today it has to be retrofitted at checkout. Most legacy Indian PMS (Opera, IDS Next) capture GSTIN at check-in as a first-class field. Until the check-in DTO is extended, checkout is the last chance to capture it before the IRN queue fires.
- For B2C guests (no GSTIN), the buyer block on the IRN payload uses
gstin='URP'(Unregistered Person). For inter-state B2C, IGST applies — but D-011 (P1) flags that today'sGstr1ExportServiceuses property state instead of guest billing state for B2C place-of-supply, leading to mis-classification.
Step 3: Settle outstanding balance (cash / card / UPI / CITY_LEDGER)
- Story: US-005-04 (Folio Payment Recording).
- Code:
hmp-frontdesk-service/src/folios/folios.controller.ts—POST /folios/:id/payments(@Roles('SUPER_ADMIN','ADMIN','MANAGER','FRONT_DESK','ACCOUNTS','F_AND_B','RESERVATIONS')). Service: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 viaCurrencyClientService; atomic FolioTransaction(PAYMENT, negative amount) + Payment row + folio balance update. For UPI QR + Razorpay terminal + payment-link captures, the path is producer-driven viaPaymentCapturedConsumer(Task #11 2026-05-19, Gotcha 27). Split-payment at checkout:hmp-frontdesk-service/src/checkout/dto/checkout.dto.ts:SplitPaymentLineDto(post-PMS-2 fix Gotcha 0b:@IsArray()+@ValidateNested({each:true})). - Test:
folios.service.spec.tspostPayment cases (0/NaN reject, negative refund P0-1, overpayment trigger, multi-currency);consumers/payment-captured.consumer.spec.ts(20 cases);checkout/split-payment.spec.ts. Flow:flow-8-check-out.spec.ts:: "POST/api/folios/:id/paymentssettles the outstanding balance" — 0/15 PASS at snapshot;advance-deposit-split.flow.spec.ts(5 scenarios);terminal-folio-settlement.flow.spec.ts(6 scenarios). - Pre-conditions: Steps 1, 2 completed. Folio
status=OPEN. Caller has settlement-role authority. For CITY_LEDGER mode,Companyrow exists with credit-limit headroom (outstanding + amount <= creditLimitper D-005B-04 SME-PROPOSED). - Post-conditions: One or more
FolioTransaction(transactionType='PAYMENT', amount=-X)rows. Folio balance updated atomically. For Razorpay paths, the negative-payment row is produced byPaymentCapturedConsumer(not the direct POST) — dedup keyfolio:{folioId}:payment-captured:{paymentId}prevents double-application. - Notes:
- Payments are stored as negative amounts (signed:
balance = totalCharges + sum(payments)). Non-obvious for new engineers — refund vs payment vs negative-payment must be explicit in the spec. PaymentCapturedConsumeris the only supported path for Razorpay captures (4 producer paths: booking-engine redirect, terminal-charge worker, payment-link webhook, UPI QRqr_code.creditedwebhook). Direct POST/folios/:id/paymentsremains for cash + manual front-desk entries.- D-004 (P1): today's
paymentRefidempotency is atFolioTransaction.referencededup shape, NOT at a DB unique constraint onPayment(folioId, paymentMode, paymentRef). A direct retry ofPOST /folios/:id/paymentswith the samepaymentRefwould create a duplicate Payment row. SME-PROPOSED unique partial index. - D-006 (P1): settle requires exact-zero balance. A 1-paise rounding residual blocks settle —
[SME-PROPOSED]Property.settlementRoundingTolerance(default 1.00 INR) + auto-ROUNDINGadjustment.
- Payments are stored as negative amounts (signed:
Step 4: Manager approval for high-value / force-checkout
- Story: US-005-002 AC plus
[SME-PROPOSED]new story —US-005-NEW-FORCE-CHECKOUT-APPROVAL"Manager PIN gate for force-checkout and large refunds". Today: anyFRONT_DESKrole can passforceCheckout: trueonPOST /check-out/:reservationIdand bypass the balance gate. Real-world hotels (KeyNova MUM, Vishramgruh) require manager/supervisor approval — empirically, the threshold is ₹10 lakh (10,00,000 INR) at MUM. - Code:
hmp-frontdesk-service/src/checkout/checkout.controller.ts:POST /check-out/:reservationId(acceptsforceCheckout: boolean). Today no PIN / authorization-code gate is enforced beyond the@Rolesdecorator. The auth-servicemanager-pinsurface is the closest existing primitive — verify in code. - Test:
checkout.service.spec.tscoversforceCheckout: truepath but does NOT assert role-required override. SME-PROPOSED spec: a non-MANAGER role callingforceCheckout: truemust 403 withFORCE_CHECKOUT_REQUIRES_MANAGER_APPROVAL. - Pre-conditions: Folio balance > 0 OR balance >
Property.policies.managerApprovalThreshold(proposed; default ₹10,00,000). Caller role gated. - Post-conditions:
Reservation.checkedOutBypopulated.Folio.forceClosedReason+Folio.forceClosedBy=<managerUserId>+ audit row with actor role + (PIN-issuer if PIN-authorised). If PIN-authorised, the time-bound 6-digit PIN must be validated against the property's active manager PINs. - Notes:
- D-005A-010 (P1):
forceCheckout: trueforFRONT_DESK_AGENTshould 403 withFORCE_CHECKOUT_REQUIRES_MANAGER_APPROVAL. Today it silently succeeds. - At KeyNova MUM the soft threshold is ₹10L because that's the wedding-block deposit ceiling that triggers C-level review per the property's house rules. Property-configurable per
Property.policies.managerApprovalThreshold.
- D-005A-010 (P1):
Step 5: Folio close (status → SETTLED, payments applied)
- Story: US-005-06 (Folio Settlement).
- Code:
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 carryingsegment / countryCode / roomTypeCode / ratePlanCode / roomNights / taxAmount).POST /folios/:id/settle. Checkout pipeline:checkout.service.ts:checkOutcallssettleFolioper open folio. - Test:
folios.service.spec.ts:: settleFolio cases (zero-balance success, reject with balance, reject already-settled, reject voided folio);fu-55-folio-event-dual-emit.spec.ts(8 cases — legacy + canonical event shapes + remap oftotalAmount/closedAt/segment/countryCode/taxAmount). Flow:flow-8-check-out.spec.ts(settle-to-SETTLED its) — 0/15 PASS at snapshot. - Pre-conditions: Steps 3, 4 completed. Folio balance = 0 (exact, today — see D-006). Folio not already SETTLED / VOID / TRANSFERRED.
- Post-conditions:
Folio.status=SETTLED + settledAt=now().folio.settled(legacy) ANDfolio.closed(canonical with FU-55 enrichments) published. Downstream consumers: analyticsMvRevenueBySource + MvDailySalesRegister + MvGstr3bSnapshotpopulate fromfolio.posted(FU-55 closed 5 empty MVs). - Notes:
- Settlement is final — no new charges. Recourse is
voidTransaction(manager+) orCreditNote(US-005B-02 / AP-T08-007 queue-routed). - FINDING-008 fix: each settled folio surfaces its IRN in the checkout response (verified in
check-out-einvoice.flow.spec.ts). - Until the Track-4 retirement of
folio.settled(legacy) lands, both events must publish. Consumers should subscribe to the canonicalfolio.closedonly.
- Settlement is final — no new charges. Recourse is
Step 6: Reservation status → CHECKED_OUT
- Story: US-005-002 (Standard Check-Out).
- Code:
hmp-frontdesk-service/src/checkout/checkout.service.ts:checkOut— flipsReservation.status=CHECKED_OUT + checkedOutAt=now() + checkedOutBy=<userId>, publishesRESERVATION_CHECKED_OUT(cross-service) +ROOM_STATUS_CHANGED+HOUSEKEEPING_TASK_CREATED. - Test:
checkout.service.spec.ts(full checkout + balance enforcement + force-checkout + multi-folio);checkout-dto-validation.spec.ts(PMS-2 fix Gotcha 0b — 8 specs);express-checkout.spec.ts;sponsored-billing.service.spec.ts(Vishramgruh sponsor split). Flow:flow-8-check-out.spec.ts(it: "reservation status becomes CHECKED_OUT after checkout") — 0/15 PASS at snapshot. - Pre-conditions: Step 5 completed (all OPEN folios on reservation are SETTLED or force-closed). Reservation status
CHECKED_IN. - Post-conditions:
Reservation.status=CHECKED_OUT. Reservation-service consumesRESERVATION_CHECKED_OUTand updates its own canonical row.GuestRetentionTimer(DPDPA) starts the 90-day retention countdown (per memory: AP-T08-005 GuestErasedConsumer wires the right side). - Notes:
- The 2026-05-16 snapshot's flow-8 root cause was "frontdesk nationality fallback" — a slim-read-model Guest lookup 5xx'd when stale. QA-CHECKIN D1 reconciler (2026-05-22 Gotcha 28) addresses the equivalent on check-in; check the symmetry for check-out before claiming D-005A-008 closed.
Step 7: Room status OCCUPIED → DIRTY (HK pickup queue)
- Story: US-005-002 + US-006-001 (Room Status / Housekeeping). Cross-service: frontdesk emits, housekeeping + property consume.
- Code:
checkout.service.ts:checkOutpublishesROOM_STATUS_CHANGED(payload setshousekeepingStatus=DIRTYANDstatus=VACANT_DIRTY) andHOUSEKEEPING_TASK_CREATED. Property-service consumer flips both columns. Housekeeping board surfaces the room under "Needs service". - Test: covered by
flow-5-fd-hk-updates.spec.ts(11/12 PASS at snapshot — most reliable flow on the platform). Flow-8 also asserts the room transition. - Pre-conditions: Step 6 completed.
- Post-conditions:
Room.status=VACANT_DIRTY.Room.housekeepingStatus=DIRTY. HK task row created with default priority + assigned to the floor's default housekeeper rotation. - Notes:
- D-005A-004 (P0) flagged for check-in (room HK status not flipped). Verify the symmetric check-out flow flips BOTH columns (
statusANDhousekeepingStatus). If onlystatusflips, EP-006 housekeeping board's "occupied & needing service" filter double-counts.
- D-005A-004 (P0) flagged for check-in (room HK status not flipped). Verify the symmetric check-out flow flips BOTH columns (
Step 8: E-invoice IRN generation (async queue to NIC IRP)
- Story: US-005-11 / catalog US-005-007 (GST E-Invoice Generation).
- Code:
hmp-frontdesk-service/src/einvoice/einvoice.service.ts:generateIrnForFolio— auto-fires fromcheckout.service.ts:436(Gotcha 8 / per per-step finding 008). Enqueues anEInvoiceJob(documentType='INV')row;EInvoiceJobProcessor(cron every minute, 5 jobs/batch, 5-min retry backoff,maxRetries=3, DLQ on exceed) picks it up. Adapter resolution:EINVOICE_MOCK=true → MockAdapter;COMPLIANCE_MODE=live + creds → NicAdapter; else MockAdapter with warning. Production boot guard (AP-T08-001 / 2026-05-19): factory 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). Demo-tenant guard (Plan 4 2026-05-13): whenTenant.isDemoTenant=true, NIC submission is SKIPPED regardless ofEINVOICE_MOCK— stricter than env flag. - Test:
einvoice.service.spec.ts(generateIrnForFolio+ B2B GSTIN AP-T08-003);einvoice.module.spec.ts(12 factory cases);einvoice-job.processor.spec.ts(INV-vs-CRN branch + retry + DLQ). Flow:flow-8-check-out.spec.ts(its: "POST/api/einvoice/generateis reachable", "response contains a jobId and QUEUED status", "completed job carries GST amounts") — 0/15 PASS at snapshot;check-out-einvoice.flow.spec.ts;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). - Pre-conditions: Steps 5, 6 completed. Folio SETTLED with GST split persisted (AP-T07-001 / W2.A.1 columns populated). For B2B:
Folio.buyerGstinnon-null (else 422B2B_REQUIRES_BUYER_GSTINper D-005A-009 SME-PROPOSED). Not a demo tenant (elseSKIPPED_DEMO_TENANTlog). - Post-conditions:
EInvoiceJobrow withstatus='QUEUED', documentType='INV'. Processor picks up within ≤ 60s; calls NIC/eivital/v1.04/auth(token cached until expiry) then IRN-generation endpoint. On success:EInvoiceJob.status='COMPLETED' + irn + ackNumber + signedQRCode + ackDate. - Notes:
- B2B
company: truePrisma include is load-bearing (Gotcha 6 / AP-T08-003). If dropped, every corporate booking silently regresses to B2C withbuyer.GSTIN='URP'— breaks GSTR-1/GSTR-2B reconciliation for every corporate account. Regression-spec guard required. - NIC IRP rate limits: 1000 IRN/sec sustained, 2500 burst per GSTIN (NIC e-invoice IRP v1.04 docs). D-005 (P1, EP-012C):
NicEInvoiceAdapterhas a 30-s timeout but noRetry-Afterheader parsing, no exponential backoff on HTTP 429, no per-GSTIN concurrency cap. A night-audit batch run could saturate. - IRN 24-hour cancellation window (NIC policy):
POST /einvoice/cancelmust be allowed only whennow() - Folio.ackDate <= 24h; beyond → 422IRN_CANCEL_WINDOW_EXPIRED+ suggestPOST /einvoice/credit-note(AP-T08-007). D-005A-011 (P1) SME-PROPOSED. - CGST+SGST vs IGST is derived at folio post-time from
customerStateper AP-T07-004 — intra-state → CGST+SGST; inter-state → IGST. B2C path bug:Gstr1ExportServiceuses property state instead of guest billing state for B2C place-of-supply — D-011 (P1, EP-012C).
- B2B
Step 9: IRN response (SLA: AC says ≤30s; reality 5-min retry backoff — DRIFT)
- Story: US-005-11 AC +
[SME-PROPOSED]refinement. - Code:
EInvoiceJobProcessorruns every minute; on transient NIC failure, retries with 5-min backoff up tomaxRetries=3; on exceed → DLQ (EInvoiceJob.status='DEAD_LETTER'). DLQ recovery:POST /einvoice/dead-letter/:jobId/retry. Webhook callback receiver:einvoice-webhook.controller.tsfor NIC async callbacks. Compliance report:GET /einvoice/compliance-report(P0-16 buckets: filed / queued / processing / deadLetter / missing). - Test:
einvoice-job.processor.spec.ts(retry + DLQ + INV/CRN branch). - Pre-conditions: Step 8 completed (job enqueued).
- Post-conditions: Within ≤ 60s first attempt (mock) or ≤ 90s (NIC sandbox/live), the job either COMPLETES or transitions to PROCESSING for retry. After 3 failed attempts → DEAD_LETTER.
- Notes:
- D-008 (P0, EP-012C): EPIC AC says "IRN within 30 seconds" but the queue is async with 5-min retry backoff. A transient NIC outage will visibly violate the SLA. SME-PROPOSED: split into (a) p95 first-attempt SLA (30s mock / 90s NIC), (b) reconciliation SLA (24h via
getComplianceReportdaily run; DEAD_LETTER reviewed within 1 business day). - D-007 (P0, EP-012C): demo-tenant skip leaves NO
EInvoiceJobrow — observable only via structured log. Compliance evidence cannot prove "demo tenant — intentionally skipped" without parsing logs. SME-PROPOSED: addEInvoiceJob.status='SKIPPED_DEMO_TENANT'enum + migration so the daily report can include ademoSkippedbucket.
- D-008 (P0, EP-012C): EPIC AC says "IRN within 30 seconds" but the queue is async with 5-min retry backoff. A transient NIC outage will visibly violate the SLA. SME-PROPOSED: split into (a) p95 first-attempt SLA (30s mock / 90s NIC), (b) reconciliation SLA (24h via
Step 10: Folio.irn denormalised (AP-T08-006)
- Story: US-005-11 + AP-T08-006 (Gotcha 20).
- Code:
einvoice-job.processor.tson COMPLETED writesFolio.irn + signedQRCode + ackDate + ackNumberviadocumentType-aware completion (AP-T08-007: INV → Folio columns; CRN →CreditNote.creditNoteIrn + status=IRN_GENERATED— CRN must NOT touch Folio.irn). - Test:
einvoice-job.processor.spec.ts(CRN-vs-INV branch);folios/invoice-pdf.service.spec.ts(re-aligned post-Top-10 closure 2026-05-20 — canonicalFoliosService.getNextInvoiceNumberis the auto-numbering owner, NOT the PDF service). - Pre-conditions: Step 9 reached COMPLETED.
- Post-conditions:
Folio.irnpopulated. PDF endpointGET /folios/:id/invoice/pdfreadsfolio.irndirectly (no longer hitsEInvoiceJobper AP-T08-006) and renders the real IRN + QR code visually. Watermark'DRAFT — IRN PENDING'when IRN still null. Backfill script for legacy rows:prisma/migrations-data/backfill-folio-irn.sql. - Notes:
- The PDF service has the data (
folio.irn,folio.signedQRCode) — D-005B-08 SME-PROPOSED: spec must encode "PDF SHALL render IRN as text + visual QR (Base64-decoded fromfolio.signedQRCode) whenfolio.irn IS NOT NULL; watermark when null". - AP-T08-007 (Gotcha 29): Credit-note IRN is queue-routed via
EInvoiceJob.documentType='CRN'. Idempotent per(folioId, creditNoteNumber). DEAD_LETTER/FAILED jobs intentionally NOT matched byfindActiveCreditNoteJobso a permanently-failed CRN can be re-queued.
- The PDF service has the data (
Step 11: Receipt PDF generated + emailed (SES via IRSA)
- Story: US-005B-08 (Folio PDF Export & Email).
- Code:
hmp-frontdesk-service/src/folios/invoice-pdf.service.ts—pdfkit-based GST-compliant invoice PDF; renders DRAFT watermark when IRN null.GET /folios/:id/invoice/pdf—@Res() res: Response+res.end(buffer)pattern. Email dispatch: via@hmp/messagingevent →hmp-notification-serviceconsumer → AWS SES (per memory 2026-05-21 migration: SES authenticates via IRSA, NO API key). SES module Terraform:terraform/modules/ses-email/(OP-1 + OP-2). Sender identity:mail.<env>.keynova.ai(DKIM RSA 2048 + custom MAIL FROM + SPF + DMARC). SA annotation:hmp-notification-serviceServiceAccount getseks.amazonaws.com/role-arnpatch per env. - Test:
invoice-pdf.service.spec.ts. Flow:email-invoice-delivery.flow.spec.ts(4 scenarios — folio create, EMAIL NotificationLog within 5s, sender metadata, notification-service health). Branded receipts:hmp-shared-packages/packages/common/src/printing/receipt-templates.ts(generateFolioReceipt,generatePosReceipt— 48-char ESC/POS with KeyNova brand header, full GST breakdown CGST/SGST/IGST, HSN/SAC codes, IST date format, Indian number grouping, IRN support — 47 byte-level tests). - Pre-conditions: Step 10 completed. Guest has email on file. SES sender identity verified (
VerifiedForSendingStatus: true). SES out of sandbox in prod (OP-3 manual support case). - Post-conditions: PDF buffer returned to dashboard. NotificationLog row created. Email queued via SES (or DLQ if SES outage). Branded receipt printed at front-desk thermal printer (if hardware present).
- Notes:
- D-005B-08 SME-PROPOSED: email dispatch should trigger on
folio.closed, NOTreservation.created(today's AP-CC-5 flow). Guest wants the invoice at checkout, not at booking. - For B2B (company-linked) folios, email SHALL CC the company billing-contact (per
Company.billingEmail), not justreservation.guest.email. Today's path likely uses guest email only. - FU-51 SUPERSEDED 2026-05-21 by SendGrid → AWS SES migration. There is no longer a SendGrid prod key to provision. OP-2 / FU-30 is the BLOCKER: SES IRSA role + ServiceAccount annotation +
mail.keynova.aisender identity verification. Without it the first property's email dispatch fails (AP-CC-5 prod boot guard now requiresSES_FROM_EMAIL). - Email retry + DLQ live in
hmp-notification-service. Verify the folio-invoice event survives a notification-service outage.
- D-005B-08 SME-PROPOSED: email dispatch should trigger on
Step 12: Guest stays count incremented + loyalty earn
- Story: US-008-NNN (Loyalty Tier — out of scope EP-005 but cross-EPIC; catalog row exists under EP-008 Guest CRM).
- Code: CRM-service consumer of
RESERVATION_CHECKED_OUTincrementsGuest.staysCount + lifetimeRevenueand recalculatesGuest.loyaltyTierper the property's tier ladder (Bronze / Silver / Gold / Platinum at KeyNova MUM; Trust-Donor tiers at Vishramgruh). Cross-service via canonical event; idempotent via Redis SETNX dedup onreservation.id. - Test:
hmp-crm-serviceconsumer specs (verify; not in BF-005 scope). - Pre-conditions: Step 6 completed.
- Post-conditions:
Guest.staysCount += 1.Guest.lifetimeRevenue += folio.totalAmount.Guest.loyaltyTierre-evaluated. If the guest crossed a tier boundary, aguest.tier.upgradedevent fires for the welcome-back templating engine (V2). - Notes:
- This is the cross-EPIC seam BF-005 spans into BF-007 (CRM / Loyalty). The CRM consumer must dedupe per reservation — if batch check-out (US-005A-01) fires bulk events, the consumer's idempotency guard prevents double-earn. D-005A-001 SME-PROPOSED: "Batch check-in SHALL publish one
RESERVATION_CHECKED_INevent per reservation (not one bulk event)" — symmetric requirement applies for batch check-out.
- This is the cross-EPIC seam BF-005 spans into BF-007 (CRM / Loyalty). The CRM consumer must dedupe per reservation — if batch check-out (US-005A-01) fires bulk events, the consumer's idempotency guard prevents double-earn. D-005A-001 SME-PROPOSED: "Batch check-in SHALL publish one
Failure modes worth testing
| # | Scenario | Expected behaviour | Coverage today | Owning story / drift |
|---|---|---|---|---|
| F1 | NIC IRP down (HTTP 5xx / timeout) | EInvoiceJob retries with 5-min backoff; after 3 attempts → DEAD_LETTER. Folio stays SETTLED; PDF renders 'DRAFT — IRN PENDING' watermark. Operator drains DLQ via POST /einvoice/dead-letter/:jobId/retry. | einvoice-job.processor.spec.ts (retry + DLQ branches) — PASS. nic-einvoice-live.flow.spec.ts covers adapter coexistence. | US-005-11 / D-005 P1 (Retry-After missing) |
| F2 | IRN job retry exhausted | EInvoiceJob.status='DEAD_LETTER'. GET /einvoice/dead-letter surfaces the job. Compliance report shows it in the deadLetter bucket. Front-desk dashboard EInvoiceStatusCard.tsx shows red banner. | einvoice-job.processor.spec.ts — PASS. getComplianceReport (P0-16) — PASS. | US-005-11 |
| F3 | Manager not available for force-checkout | Today: silently succeeds for FRONT_DESK role (BUG). SME-PROPOSED: 403 with FORCE_CHECKOUT_REQUIRES_MANAGER_APPROVAL. | checkout.service.spec.ts covers happy-path; missing role-required spec. | D-005A-010 P1 |
| F4 | Payment partial — balance > 0 at checkout | POST /check-out/:reservationId returns 422 with code=FOLIO_BALANCE_NOT_ZERO. Wizard surfaces the residual balance and prompts for more payment / force-checkout. | checkout.service.spec.ts (balance enforcement) — PASS. Flow-8 S4. | US-005-002 |
| F5 | Refund needed (overpayment / dispute / goodwill) | POST /folios/:id/payments with negative amount (P0-1/P0-3) posts a REFUND transaction. Razorpay path: payment-service refund API → payment.captured.refund (verify producer-side wiring) → PaymentCapturedConsumer posts negative row. Cash path: missing PettyCashDisbursement linkage (D-005B-05 SME-PROPOSED). | folios.service.spec.ts (negative refund); payments.flow.spec.ts Scenario 7 (over-refund rejected). | US-005B-05 P1 |
| F6 | Late charges post-checkout (after Step 6 / Step 7) | Today: Folio.status=SETTLED → postCharge returns 409. Operator must voidTransaction the SETTLED status (manager+) OR issue a credit-note (US-005B-02 / AP-T08-007). Real-world: phone bill / minibar audit fires after physical checkout — needs a "post-checkout adjustment" surface. | Implicit in settleFolio invariant tests. NO dedicated post-checkout-adjustment flow. | [SME-PROPOSED] US-005-NEW-POST-CHECKOUT-ADJ |
| F7 | Walk-out without settle (no-show at checkout) | Reservation stays CHECKED_IN; folio stays OPEN. Operator manually marks NO_SHOW via POST /reservations/:id/no-show (verify). Late-checkout fee posts. Eventually NIGHT_AUDIT writes it off via DEAD_TENANT process (EP-005C). | noshow-late-checkout.flow.spec.ts (FT-9). | EP-005C (out of BF-005 scope) |
| F8 | Buyer GSTIN mismatch with state | Folio.buyerGstin[0:2] != property.state AND IGST already split as CGST+SGST → 400 with BUYER_GSTIN_STATE_MISMATCH. | SME-PROPOSED — no spec today. | D-005A-009 P0 |
| F9 | IRN cancellation after 24h window | Today: cancellation request silently fails at NIC (FAILED job branch). SME-PROPOSED: pre-validate now() - Folio.ackDate <= 24h; else 422 IRN_CANCEL_WINDOW_EXPIRED + suggest credit-note path. | No spec today. | D-005A-011 P1 |
| F10 | Demo tenant tries to file real IRN | Plan 4 2026-05-13 demo-tenant guard (Gotcha 26) SKIPS NIC submission regardless of EINVOICE_MOCK. Structured log DEMO_TENANT_EINVOICE_SKIPPED. AUDIT GAP: no EInvoiceJob row produced. SME-PROPOSED: add status='SKIPPED_DEMO_TENANT' for SQL-queryable evidence. | Verified by demo-tenant guard spec. | D-007 P0 (EP-012C) |
| F11 | Inter-state B2C guest — IGST mis-routed as CGST+SGST | Today: Gstr1ExportService uses property state for B2C place-of-supply (BUG). Maharashtra property + Karnataka B2C guest → emits CGST+SGST instead of IGST. | SME-PROPOSED. No spec. | D-011 P1 (EP-012C) |
| F12 | Rounding residual blocks settle | 1-paise residual from currency conversion or 5% GST on ₹1 charge blocks POST /folios/:id/settle. SME-PROPOSED: Property.settlementRoundingTolerance (default ₹1.00) + auto-ROUNDING adjustment. | No spec. | D-006 P1 (EP-005B) |
| F13 | Express auto-checkout (US-005A-03) not wired into night audit | The "express" feature (auto-settle zero-balance departures during night audit) is the missing piece. POST /check-out/express exists as manual one-off, not as batch auto-settle. | express-checkout.spec.ts covers manual; nothing for batch. | US-005A-03 Stub |
Drift summary
| ID | Source RTM | Type | Severity | Description | Surfaced by step |
|---|---|---|---|---|---|
| D-005A-008 | EP-005A | Test-vs-code + launch-blocker | P0 | flow-8 (15 scenarios) was 0/15 PASS at 2026-05-16 — worst result on the platform. Re-measurement required after D1 reconciler (2026-05-22) + FU-55 dual-emit (2026-05-23) + AP-T07-004 GST split (2026-05-23) + AP-T08-006 Folio.irn denorm. | Steps 3–9 |
| D-005A-009 | EP-005A | Missing-validation + India-compliance | P0 | Folio.buyerGstin not capturable at check-in (D-005A-005); today's checkout has no model field / validation / GSTIN-state cross-check against Property.gstin. B2B IRN gen requires buyerGstin — 422 B2B_REQUIRES_BUYER_GSTIN. | Step 2 |
| D-005A-010 | EP-005A | Authorization-bypass | P1 | forceCheckout: true on POST /check-out/:reservationId lets any FRONT_DESK role bypass the balance gate. Real-world hotels require manager PIN / supervisor approval. | Step 4 |
| D-005A-011 | EP-005A | Story-incomplete + India-compliance | P1 | Story does not enumerate IRN cancellation rules (NIC 24-h window; after that requires credit note). No AC for the 24-h window. | Step 9 |
| D-005A-004 | EP-005A | Cross-service-handoff | P0 | Room housekeeping status does not transition to DIRTY on check-in (symmetric concern for check-out). Property-service flips Room.status but leaves housekeepingStatus. Verify check-out flips BOTH columns. | Step 7 |
| D-004 | EP-005B | Story-incomplete + producer-side | P1 | paymentRef idempotency at FolioTransaction.reference dedup, NOT at a DB unique constraint on Payment(folioId, paymentMode, paymentRef). Direct retry of POST /folios/:id/payments with same paymentRef creates duplicate Payment row. | Step 3 |
| D-006 | EP-005B | Story-incomplete + UX-gap | P1 | Settle requires exact-zero balance. A 1-paise rounding residual blocks settle. SME-PROPOSED Property.settlementRoundingTolerance + auto-ROUNDING adjustment. | Step 5 |
| D-007 | EP-005B + EP-012C | Stale-review + India-compliance + AUDIT-GAP | P0 | reviews/EP-005/US-005-007-review.md (2026-03-07) says "PARTIAL 2/10" but every gap has shipped (B2B GSTIN, queue-routed CRN, Folio.irn denorm, demo-tenant skip, compliance report). Reviewer trusting the file would reopen ~10 closed items. Demo-tenant skip leaves NO EInvoiceJob row — compliance evidence requires log parsing. | Steps 8–10 |
| D-008 | EP-012C | Story-incomplete + cross-service-handoff | P0 | EPIC AC says "IRN within 30 seconds" but the queue is async with 5-min retry backoff. Transient NIC outage will visibly violate the SLA. Split into (a) first-attempt SLA, (b) reconciliation SLA. | Step 9 |
| D-005 | EP-012C | Story-incomplete + integration-risk | P1 | NIC IRP rate-limit handling missing: nic-einvoice.adapter.ts has 30-s timeout but no Retry-After header parsing, no exponential backoff on HTTP 429, no per-GSTIN concurrency cap. Night-audit batch could saturate 1000 IRN/sec limit. | Step 8 |
| D-009 | EP-012C | Story-incomplete + India-compliance | P0 | CreditNote IRN generation path shipped end-to-end (queue, retry, DLQ, idempotency) but the GSTR-1 export does not emit a Section 9 CDNR block referencing the original invoice. Cancellation processed through HMP creates CreditNote with valid IRN but never reaches GST portal. | Step 10 (credit-note path) |
| D-010 | EP-012C | Story-incomplete + India-compliance | P1 | No central rate-tier resolver. DEFAULT_GST_RATE = 18 in einvoice + per-site rate selection in folios/POS. Tariff-band logic (0% if ≤₹1,000/night, 12% if ≤₹7,500, 18% if >₹7,500 per notification 11/2017-CT-Rate) is implicit. Room sold at ₹999/₹1,001/₹7,500/₹7,501 boundary can be misclassified silently. | Step 8 |
| D-011 | EP-012C | Story-incomplete + India-compliance | P1 | B2C inter-state IGST path missing. Gstr1ExportService uses property state for B2C place-of-supply instead of guest billing state. Maharashtra property + Karnataka B2C guest → CGST+SGST instead of IGST. Affects per-transaction tax AND GSTR-1 Section 7 classification. | Step 8 |
| D-005B-08-EMAIL | EP-005B | Story-incomplete + UX | P1 | Email dispatch flow triggers on reservation.created, not folio.closed. Guest expects invoice at checkout, not at booking. Re-target AP-CC-5 flow. B2B emails do not CC company billing-contact. | Step 11 |
| D-005A-001-SYM | EP-005A | Story-incomplete + cross-service | P1 | Batch check-in must publish one event per reservation (not one bulk). Symmetric requirement for batch check-out — CRM loyalty consumer must dedupe per reservation. | Step 12 |
| D-001 (EP-005B) + D-001 (EP-012C) | both | Catalog-vs-spec numbering | P0 | Two parallel story-ID schemes in BOTH EP-005B (catalog US-005-003/007/017 vs EPIC US-005-03..06 + US-005-11 + US-005B-01..08) and EP-012C (catalog flat US-012-001..020 vs spec US-012-04/05/14 + US-012C-01..06). Reviewer asking "is US-005B-04 built?" gets no answer from catalog. Pilot-blocking for BF-005. | All steps |
P0 launch-blocker findings (consolidated, BF-005 scope):
- D-005A-008 — flow-8 0/15 PASS launch blocker; require ≥12/15 PASS before EP-005A is launch-ready
- D-005A-009 — buyer-GSTIN capture missing; B2B IRN cannot fire cleanly for corporate folios
- D-005A-004 — room HK status not flipped on check-out (symmetric to check-in); breaks EP-006 board
- D-007 — stale e-invoice review file + demo-tenant skip AUDIT GAP (no SQL-queryable evidence)
- D-008 — IRN SLA AC misrepresents async-queue reality; operator dashboard trust at stake
- D-009 — CreditNote IRN generated but GSTR-1 Section 9 CDNR export missing; GST portal never sees cancellations
- D-001 (×2) — dual numbering schemes block BF-005 sign-off; catalog vs sub-EPIC story ID mismatch
Cross-EPIC narrative summary
| Step | Spans | Why cross-EPIC |
|---|---|---|
| 1 | EP-005B (folios) + EP-004 (POS) + EP-010 (Spa) + EP-009 (Banquet) | Pre-checkout review aggregates charges from every revenue-producing service via async consumers. |
| 2 | EP-005A (check-out) + EP-008 (CRM Companies) | Buyer GSTIN cross-references the CRM Company row. Mismatch returns 400 with BUYER_GSTIN_COMPANY_MISMATCH. |
| 3 | EP-005B (folio payment) + EP-015 (payment-service Razorpay) | Razorpay captures flow through PaymentCapturedConsumer (Task #11). Cash + manual remain direct POST. |
| 4 | EP-005A (check-out) + EP-001 (auth roles + manager PIN) | Force-checkout requires MANAGER role + (optional) time-bound PIN. PIN surface is auth-service-owned. |
| 5 | EP-005B (settle) + EP-012 (analytics MVs) | FU-55 dual-emit feeds 5 analytics MVs from folio.posted + folio.closed canonical events. |
| 6 | EP-005A (check-out) + EP-003 (reservation-service) + EP-008 (CRM retention) | CHECKED_OUT mirrors back to reservation-service; CRM GuestRetentionTimer starts DPDPA 90-day countdown. |
| 7 | EP-005A (check-out) + EP-002 (property rooms) + EP-006 (housekeeping) | ROOM_STATUS_CHANGED flows to property-service + housekeeping. Symmetric to D-005A-004 check-in concern. |
| 8 | EP-005B (e-invoice) + EP-002 (property GSTIN) + EP-008 (Company GSTIN) | IRN generation reads property GSTIN (supplier) + folio.company.gstin (buyer) or Folio.buyerGstin ad-hoc. AP-T08-003 company: true Prisma include is load-bearing. |
| 9 | EP-005B (queue) + EP-012C (compliance report) | Retry/DLQ surfaces via getComplianceReport (P0-16) — daily Ops drain. |
| 10 | EP-005B (Folio.irn denorm) + EP-012C (CDNR Section 9 export) | AP-T08-006 + AP-T08-007 close the per-invoice loop; D-009 closes the per-credit-note loop. |
| 11 | EP-005B (PDF) + EP-014 (notification-service + SES) | Email dispatch via SES IRSA (OP-1/OP-2); D-005B-08-EMAIL flags the wrong trigger event. |
| 12 | EP-005A (CHECKED_OUT event) + EP-008 (CRM loyalty) | Loyalty earn deduped per reservation via Redis SETNX. Symmetric to D-005A-001 batch-event requirement. |
The e-invoice queue (Step 8–10) is the cross-cutting seam that ties EP-005B (folio settlement) to EP-012C (GSTR-1/3B reporting). The 24-h IRN cancellation window + 24-h credit-note path are the only recourse for post-IRN corrections, and both share the same async queue substrate. The five P0 drifts on this seam (D-005A-008, D-005A-009, D-007, D-008, D-009) are the BF-005 launch-blocker spine.
Day-2 / Day-30 follow-ups (NOT in BF-005 scope)
- GSTR-3B ITC integration — purchase invoices from
hmp-asset-servicevendor master (D-006 P0 EP-012C). Today HMP files net-tax-payable as gross output tax (OVERPAY). - Regional tax (Goa tourism levy 5%, Maharashtra luxury tax >₹7,500, Rajasthan tourism development fee) — US-012C-03 Not Started. P0 for any launch in Goa/Maharashtra/Rajasthan/J&K (BF-005 marks P1 today; bump to P0 per property).
- Compliance calendar (US-012C-04 Not Started) — GSTR-1 by 11th, GSTR-3B by 20th, TDS by 7th, e-invoice IRN 24-h cancellation, Form 26AS quarterly, ESIC/EPF 15th, professional tax monthly.
- Express auto-checkout from night audit (US-005A-03 Stub) — wire
CheckoutService.processExpressCheckOuts(propertyId, businessDate)into the EP-005C night-audit step after room-charge posting. - Hash-chain audit log (D-008 P0 EP-012C, US-012-14) —
EventStore.entryHash + previousHashcolumns + daily backfill cron +GET /auditlog/verify. CERT-In + RBI audit requirement. - One canonical GSTR-1 implementation (D-003 P0 EP-012C) — retire
Gstr1ExportService(frontdesk live-folio scan); route/compliance/gstr1through gateway to analytics-sidegst-reportv2 (MV-backed). - Central GST rate-tier resolver (D-010 P1 EP-012C) —
@hmp/common/src/gst/rate-tier-resolver.tsconsumed by folios + POS + einvoice; eliminatesDEFAULT_GST_RATE=18magic constant. - State-specific registration card (D-005A-016 P1 EP-005A) — state-bound templates (MH, KA, TN, DL, GA + fallback) for legal-document requirement under state Hotel Acts.
See also
BF-001-tenant-property-onboarding.md— template for the cross-EPIC narrative shape../reconciliation/EP-005A-rtm.md— check-out controller RTM../reconciliation/EP-005B-rtm.md— folio settlement + e-invoice queue RTM../reconciliation/EP-012C-rtm.md— GSTR-1/3B reporting RTM../reconciliation/PLAYBOOK.md— authoring patternhmp-infrastructure/testing/flow-tests/src/flows/flow-8-check-out.spec.ts— 15-scenario launch-blocker suite (0/15 at 2026-05-16; re-measure)hmp-infrastructure/testing/flow-tests/src/flows/check-out-einvoice.flow.spec.ts— IRN happy path + idempotencyhmp-infrastructure/testing/flow-tests/src/flows/nic-einvoice-live.flow.spec.ts— adapter-coexistence prod-swap readinesshmp-docs/docs/platform-registry/2026-05-16-13-flow-demo-readiness.md— source of truth for flow pass rates (snapshot)hmp-docs/docs/platform-registry/property-1-readiness-audit-2026-04-28.md— Property #1 audit (47 P0 / 41 P1 / 39 P2)hmp-docs/docs/platform-registry/property-1-gold-standard-fix-plan-2026-04-28.md— Grand Plan + charter