Skip to main content

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​

PersonaRoleSource
FRONT_DESK_AGENTDay-shift checkout staff01-epic-catalog.md (EP-005 row)
MANAGER / ACCOUNTSForce-checkout / void / refund approverEPIC body EP-005B §3.1
Guest (B2C)Indian resident, walks up at checkoutAadhaar Act 2016
Guest (B2B)Corporate / GSTIN-bearing buyerGST CGST/IGST Acts
Compliance OfficerDaily GSTR-1/3B reconciliation, IRN compliance reportEP-012C getComplianceReport (P0-16)
Night-audit batchEP-005C K8s CronJob 02:00 IST — invokes express auto-settlekubernetes/jobs/night-audit-cronjob.yaml

Entry points​

EntryURLBacked by
Dashboard departures boardhttps://dashboard.<env>.keynova.ai/front-desk/departureshmp-web-dashboard/src/app/(dashboard)/front-desk/departures/page.tsx
Dashboard checkout wizardhttps://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 ISTkubernetes/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 of getById), 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. Folio status=OPEN. All upstream producers (POS, spa, banquet, in-room dining) have flushed their pos.charge.posted-class events and the PosFolioChargeConsumer has 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. PosFolioChargeConsumer is 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 denormalized IRN 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/placeOfSupply columns missing from the DB. AP-T07-004 migration 20260523120000_add_folio_transaction_gst_split finally added them on 2026-05-23. Re-measure post-fix.

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 reads folio.company.gstin for 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): if company: true is dropped from the folio query, every corporate booking silently regresses to B2C with buyer.GSTIN='URP', breaking GSTR-1/GSTR-2B reconciliation. Folio.buyerGstin column exists; PATCH /folios/:id/buyer-gstin route 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=COMPANY OR 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.buyerGstin populated. The first 2 digits of the buyer GSTIN drive intra-state vs inter-state for IGST resolution per AP-T07-004 — if buyerGstin[0:2] === property.gstin[0:2] → CGST+SGST; else → IGST. Mismatch with existing customerState returns 400 with code BUYER_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's Gstr1ExportService uses 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 via CurrencyClientService; atomic FolioTransaction(PAYMENT, negative amount) + Payment row + folio balance update. For UPI QR + Razorpay terminal + payment-link captures, the path is producer-driven via PaymentCapturedConsumer (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.ts postPayment 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/payments settles 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, Company row exists with credit-limit headroom (outstanding + amount <= creditLimit per 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 by PaymentCapturedConsumer (not the direct POST) — dedup key folio:{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.
    • PaymentCapturedConsumer is the only supported path for Razorpay captures (4 producer paths: booking-engine redirect, terminal-charge worker, payment-link webhook, UPI QR qr_code.credited webhook). Direct POST /folios/:id/payments remains for cash + manual front-desk entries.
    • D-004 (P1): today's paymentRef idempotency is at FolioTransaction.reference dedup shape, NOT at a DB unique constraint on Payment(folioId, paymentMode, paymentRef). A direct retry of POST /folios/:id/payments with the same paymentRef would 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-ROUNDING adjustment.

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: any FRONT_DESK role can pass forceCheckout: true on POST /check-out/:reservationId and 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 (accepts forceCheckout: boolean). Today no PIN / authorization-code gate is enforced beyond the @Roles decorator. The auth-service manager-pin surface is the closest existing primitive — verify in code.
  • Test: checkout.service.spec.ts covers forceCheckout: true path but does NOT assert role-required override. SME-PROPOSED spec: a non-MANAGER role calling forceCheckout: true must 403 with FORCE_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.checkedOutBy populated. 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: true for FRONT_DESK_AGENT should 403 with FORCE_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.

Step 5: Folio close (status → SETTLED, payments applied)​

  • Story: US-005-06 (Folio Settlement).
  • Code: hmp-frontdesk-service/src/folios/folios.service.ts:settleFolio — validates balance===0, sets status=SETTLED + settledAt=now(), publishes BOTH legacy folio.settled AND canonical folio.closed (FU-55 dual-emit 2026-05-23 carrying segment / countryCode / roomTypeCode / ratePlanCode / roomNights / taxAmount). POST /folios/:id/settle. Checkout pipeline: checkout.service.ts:checkOut calls settleFolio per 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 of totalAmount/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) AND folio.closed (canonical with FU-55 enrichments) published. Downstream consumers: analytics MvRevenueBySource + MvDailySalesRegister + MvGstr3bSnapshot populate from folio.posted (FU-55 closed 5 empty MVs).
  • Notes:
    • Settlement is final — no new charges. Recourse is voidTransaction (manager+) or CreditNote (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 canonical folio.closed only.

Step 6: Reservation status → CHECKED_OUT​

  • Story: US-005-002 (Standard Check-Out).
  • Code: hmp-frontdesk-service/src/checkout/checkout.service.ts:checkOut — flips Reservation.status=CHECKED_OUT + checkedOutAt=now() + checkedOutBy=<userId>, publishes RESERVATION_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 consumes RESERVATION_CHECKED_OUT and 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:checkOut publishes ROOM_STATUS_CHANGED (payload sets housekeepingStatus=DIRTY AND status=VACANT_DIRTY) and HOUSEKEEPING_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 (status AND housekeepingStatus). If only status flips, EP-006 housekeeping board's "occupied & needing service" filter double-counts.

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 from checkout.service.ts:436 (Gotcha 8 / per per-step finding 008). Enqueues an EInvoiceJob(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 in NODE_ENV=production + COMPLIANCE_MODE=live without any of EINVOICE_CLIENT_ID/SECRET/USERNAME/PASSWORD/GSTIN (escape hatch: EINVOICE_ALLOW_MOCK_IN_PROD=true). Demo-tenant guard (Plan 4 2026-05-13): when Tenant.isDemoTenant=true, NIC submission is SKIPPED regardless of EINVOICE_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/generate is 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.buyerGstin non-null (else 422 B2B_REQUIRES_BUYER_GSTIN per D-005A-009 SME-PROPOSED). Not a demo tenant (else SKIPPED_DEMO_TENANT log).
  • Post-conditions: EInvoiceJob row with status='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: true Prisma include is load-bearing (Gotcha 6 / AP-T08-003). If dropped, every corporate booking silently regresses to B2C with buyer.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): NicEInvoiceAdapter has a 30-s timeout but no Retry-After header 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/cancel must be allowed only when now() - Folio.ackDate <= 24h; beyond → 422 IRN_CANCEL_WINDOW_EXPIRED + suggest POST /einvoice/credit-note (AP-T08-007). D-005A-011 (P1) SME-PROPOSED.
    • CGST+SGST vs IGST is derived at folio post-time from customerState per AP-T07-004 — intra-state → CGST+SGST; inter-state → IGST. B2C path bug: Gstr1ExportService uses property state instead of guest billing state for B2C place-of-supply — D-011 (P1, EP-012C).

Step 9: IRN response (SLA: AC says ≤30s; reality 5-min retry backoff — DRIFT)​

  • Story: US-005-11 AC + [SME-PROPOSED] refinement.
  • Code: EInvoiceJobProcessor runs every minute; on transient NIC failure, retries with 5-min backoff up to maxRetries=3; on exceed → DLQ (EInvoiceJob.status='DEAD_LETTER'). DLQ recovery: POST /einvoice/dead-letter/:jobId/retry. Webhook callback receiver: einvoice-webhook.controller.ts for 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 getComplianceReport daily run; DEAD_LETTER reviewed within 1 business day).
    • D-007 (P0, EP-012C): demo-tenant skip leaves NO EInvoiceJob row — observable only via structured log. Compliance evidence cannot prove "demo tenant — intentionally skipped" without parsing logs. SME-PROPOSED: add EInvoiceJob.status='SKIPPED_DEMO_TENANT' enum + migration so the daily report can include a demoSkipped bucket.

Step 10: Folio.irn denormalised (AP-T08-006)​

  • Story: US-005-11 + AP-T08-006 (Gotcha 20).
  • Code: einvoice-job.processor.ts on COMPLETED writes Folio.irn + signedQRCode + ackDate + ackNumber via documentType-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 — canonical FoliosService.getNextInvoiceNumber is the auto-numbering owner, NOT the PDF service).
  • Pre-conditions: Step 9 reached COMPLETED.
  • Post-conditions: Folio.irn populated. PDF endpoint GET /folios/:id/invoice/pdf reads folio.irn directly (no longer hits EInvoiceJob per 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 from folio.signedQRCode) when folio.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 by findActiveCreditNoteJob so a permanently-failed CRN can be re-queued.

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/messaging event → hmp-notification-service consumer → 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-service ServiceAccount gets eks.amazonaws.com/role-arn patch 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, NOT reservation.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 just reservation.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.ai sender identity verification. Without it the first property's email dispatch fails (AP-CC-5 prod boot guard now requires SES_FROM_EMAIL).
    • Email retry + DLQ live in hmp-notification-service. Verify the folio-invoice event survives a notification-service outage.

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_OUT increments Guest.staysCount + lifetimeRevenue and recalculates Guest.loyaltyTier per 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 on reservation.id.
  • Test: hmp-crm-service consumer specs (verify; not in BF-005 scope).
  • Pre-conditions: Step 6 completed.
  • Post-conditions: Guest.staysCount += 1. Guest.lifetimeRevenue += folio.totalAmount. Guest.loyaltyTier re-evaluated. If the guest crossed a tier boundary, a guest.tier.upgraded event 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_IN event per reservation (not one bulk event)" — symmetric requirement applies for batch check-out.

Failure modes worth testing​

#ScenarioExpected behaviourCoverage todayOwning story / drift
F1NIC 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)
F2IRN job retry exhaustedEInvoiceJob.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
F3Manager not available for force-checkoutToday: 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
F4Payment partial — balance > 0 at checkoutPOST /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
F5Refund 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
F6Late 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
F7Walk-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)
F8Buyer GSTIN mismatch with stateFolio.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
F9IRN cancellation after 24h windowToday: 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
F10Demo tenant tries to file real IRNPlan 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)
F11Inter-state B2C guest — IGST mis-routed as CGST+SGSTToday: 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)
F12Rounding residual blocks settle1-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)
F13Express auto-checkout (US-005A-03) not wired into night auditThe "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​

IDSource RTMTypeSeverityDescriptionSurfaced by step
D-005A-008EP-005ATest-vs-code + launch-blockerP0flow-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-009EP-005AMissing-validation + India-complianceP0Folio.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-010EP-005AAuthorization-bypassP1forceCheckout: 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-011EP-005AStory-incomplete + India-complianceP1Story 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-004EP-005ACross-service-handoffP0Room 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-004EP-005BStory-incomplete + producer-sideP1paymentRef 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-006EP-005BStory-incomplete + UX-gapP1Settle requires exact-zero balance. A 1-paise rounding residual blocks settle. SME-PROPOSED Property.settlementRoundingTolerance + auto-ROUNDING adjustment.Step 5
D-007EP-005B + EP-012CStale-review + India-compliance + AUDIT-GAPP0reviews/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-008EP-012CStory-incomplete + cross-service-handoffP0EPIC 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-005EP-012CStory-incomplete + integration-riskP1NIC 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-009EP-012CStory-incomplete + India-complianceP0CreditNote 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-010EP-012CStory-incomplete + India-complianceP1No 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-011EP-012CStory-incomplete + India-complianceP1B2C 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-EMAILEP-005BStory-incomplete + UXP1Email 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-SYMEP-005AStory-incomplete + cross-serviceP1Batch 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)bothCatalog-vs-spec numberingP0Two 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​

StepSpansWhy cross-EPIC
1EP-005B (folios) + EP-004 (POS) + EP-010 (Spa) + EP-009 (Banquet)Pre-checkout review aggregates charges from every revenue-producing service via async consumers.
2EP-005A (check-out) + EP-008 (CRM Companies)Buyer GSTIN cross-references the CRM Company row. Mismatch returns 400 with BUYER_GSTIN_COMPANY_MISMATCH.
3EP-005B (folio payment) + EP-015 (payment-service Razorpay)Razorpay captures flow through PaymentCapturedConsumer (Task #11). Cash + manual remain direct POST.
4EP-005A (check-out) + EP-001 (auth roles + manager PIN)Force-checkout requires MANAGER role + (optional) time-bound PIN. PIN surface is auth-service-owned.
5EP-005B (settle) + EP-012 (analytics MVs)FU-55 dual-emit feeds 5 analytics MVs from folio.posted + folio.closed canonical events.
6EP-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.
7EP-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.
8EP-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.
9EP-005B (queue) + EP-012C (compliance report)Retry/DLQ surfaces via getComplianceReport (P0-16) — daily Ops drain.
10EP-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.
11EP-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.
12EP-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-service vendor 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 + previousHash columns + 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/gstr1 through gateway to analytics-side gst-report v2 (MV-backed).
  • Central GST rate-tier resolver (D-010 P1 EP-012C) — @hmp/common/src/gst/rate-tier-resolver.ts consumed by folios + POS + einvoice; eliminates DEFAULT_GST_RATE=18 magic 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 pattern
  • hmp-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 + idempotency
  • hmp-infrastructure/testing/flow-tests/src/flows/nic-einvoice-live.flow.spec.ts — adapter-coexistence prod-swap readiness
  • hmp-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