Skip to main content

BF-012 — Reporting & Compliance Export

Business flow (cross-EPIC user journey). Authored 2026-05-25 as Wave 2 of the RTM Scale-Out (1 of 12 parallel agents) per ../reconciliation/PLAYBOOK.md. Spans EP-012B (Financial Reports), EP-012C (Tax & Compliance), and EP-012D (Report Builder & Scheduled Delivery).

The journey: Finance Manager / GM picks a report from the v2 catalog (GSTR-1, GSTR-3B, Day-End, F&B, Aging, Channel Commission, …) → picks a period + property set + comparative flags → analytics-service runs the report via the Reports v2 framework → anomalies surface inline → drill down via side sheet → export PDF/XLSX/CSV → optionally save the view + schedule cron + email delivery via SES → audit row persists 5 years → (for GSTR-1) JSON ready for GSTN portal upload.

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 BF-012 reconciliation is designed to surface.

Personas​

PersonaRoleSource
Finance ManagerOwns GSTR-1, GSTR-3B, P&L, F&B cost analysis; files monthly returns by 11th / 20thEP-012C spec body §1 + 01-epic-catalog.md EP-012 surface
GM / OwnerDaily ops reports, anomaly review, pinned widget on home dashboardEP-012B spec body + pinned-reports-widget.tsx
Accountant / CAQuarterly Form 26AS, year-end audit, ITC manual override on GSTR-3BEP-012C spec body §10 + US-012-005 review
Revenue ManagerChannel commission, market segment, source-wise revenue + LY/Budget/Forecast comparatorEP-012B + Plan 5 Comparator
Compliance OfficerPolice register, FRRO C-Form, audit-log integrity verifyEP-012C US-012-14 + police-register definition
SUPER_ADMINCross-tenant viewing (for support), role-override grants for custom rolesEP-012D US-012D-02 + ReportRBACService

Entry points​

EntryURL / APIBacked by
Reports v2 catalog (browse all 63 reports)https://dashboard.<env>.keynova.ai/reports/v2hmp-web-dashboard/src/app/(dashboard)/reports/v2/page.tsx → GET /api/reports/v2/catalog
Per-report runnerhttps://dashboard.<env>.keynova.ai/reports/v2/[reportId]hmp-web-dashboard/src/app/(dashboard)/reports/v2/[reportId]/page.tsx → POST /api/reports/v2/:id/run
Saved Views listhttps://dashboard.<env>.keynova.ai/reports/v2/savedPlan 3.4 — saved-views.controller.ts
Pinned home widgethttps://dashboard.<env>.keynova.ai/PinnedReportsWidget.tsx reading saved views pinnedToHome=true
Compliance page (legacy GSTR routes)https://dashboard.<env>.keynova.ai/settings/compliancesettings/compliance/page.tsx → GET /api/compliance/gstr1 + GET /api/compliance/gstr3b (frontdesk path — see D-003 of EP-012C)
Scheduled Reports (proposed)https://dashboard.<env>.keynova.ai/reports/scheduled [SME-PROPOSED]NOT shipped — see Step 10 + EP-012D D-005
Direct API (no UI)POST /api/reports/v2/:id/run + POST /api/reports/export/:typereports-v2.controller.ts + report-exports.controller.ts

The 12-step journey​

Step 1: User picks a report from the v2 catalog​

  • Story: US-012-002 (Daily Operational Reports — Arrivals/Departures/In-House/No-Show/VIP) + US-012-04 (GSTR-1) + US-012-05 (GSTR-3B) + US-012-011 (Aging Receivables) + US-012-009 (Financial P&L — Partial) + US-012-013 (F&B Revenue) + US-012B-01 (Channel Commission) — all reachable from the same catalog grid. Catalog discovery itself is US-012D-02 (Report Catalog with Role-Based Access — Partial; no catalog row, see EP-012D D-001).
  • Code: hmp-analytics-service/src/reporting/controller/reports-v2.controller.ts — GET /reports/v2/catalog returns the 63 pre-registered ReportDefinition rows from the in-memory ReportRegistry (registered at boot by ReportDefinitionsRegistrar). Dashboard: hmp-web-dashboard/src/app/(dashboard)/reports/v2/page.tsx + components/modules/reports/v2/CatalogList.tsx.
  • Test: hmp-analytics-service/src/reporting/controller/reports-v2.controller.spec.ts — catalog endpoint shape. E2E: hmp-web-dashboard/e2e/reports-v2.spec.ts (9 specs incl. catalog mount). Flow: flow-10-reports.spec.ts :: "v2 catalog — S1..S8" — 8 reports probed; tenant-isolation S10.
  • Pre-conditions: Authenticated session; user has at least one role that maps to ≥1 report's rbac.roles.
  • Post-conditions: Catalog response delivered to the dashboard. Today the response is NOT filtered by caller role (see EP-012D D-011 [P2]) — a Housekeeping user sees the existence of gst-report and only gets 403 on click.
  • Notes: The 63-definition surface is the result of Plan 0/1/2/2.1/2.3/3 (2026-04-30) catalog expansions. Legacy per-report controllers (/api/reports/arrivals|departures|in-house|no-shows) were retired in Plan 1 — drift D-001 of EP-012B (EPIC body §4.1 still describes them).

Step 2: User selects period (today / yesterday / MTD / YTD / custom)​

  • Story: [SME-PROPOSED] new story — US-012D-NEW-PERIOD-SELECTOR "Period selector with India FY awareness (Apr–Mar)" + Indian-fiscal-month aliases. Closest existing surface is the generic validatePeriod guard at reports-v2.controller.ts:18-41 (added 2026-05-16, rejects malformed filters.period with structured 400) and the PeriodSelector UI component. Catalog has no row for fiscal-year semantics.
  • Code: hmp-analytics-service/src/reporting/controller/reports-v2.controller.ts:18-41 — validatePeriod guard. Dashboard: hmp-web-dashboard/src/components/modules/reports/v2/PeriodSelector.tsx (part of the runner page). Period shapes: {type:'today'|'yesterday'|'mtd'|'ytd'|'custom', from?:date, to?:date}.
  • Test: reports-v2.controller.spec.ts covers validatePeriod. E2E: reports-v2.spec.ts exercises period change.
  • Pre-conditions: Step 1 completed; user landed on /reports/v2/[reportId].
  • Post-conditions: filters.period payload validated at API boundary; rejected requests return structured 400 instead of crashing the resolver.
  • Notes: India FY runs Apr→March, not Jan→Dec. Today's ytd shape resolves to calendar year-to-date. Indian Finance Managers running a "YTD revenue" report in October expect April–October totals; the platform delivers Jan–October. [SME-PROPOSED] add type:'fytd' (fiscal-YTD), type:'fy', fy:'2025-26' (full FY), type:'fq', fy:'2025-26', q:1|2|3|4 (fiscal quarter). EPIC body of EP-012D AC-1 also calls for 'On 1st of FY month-N' cron support — see Step 10.

Step 3: User selects property (single or multi-property roll-up)​

  • Story: US-012-002 + US-012D-NEW-MULTI-PROP [SME-PROPOSED] — "Multi-property roll-up / portfolio aggregation across a tenant's properties". The Property Set selector ships; the cross-property aggregation semantics (currency normalization, weighted averages, GST grouping by property GSTIN) are NOT codified by any story.
  • Code: hmp-web-dashboard/src/components/modules/reports/v2/PropertySetSelector.tsx. Backend: filters.propertyIds: string[] consumed by ReportExecutionService which forwards to each def.query. Reports that aggregate across properties (e.g. revenue-monthly-report) SUM rows by property; reports that are property-scoped by definition (arrival-report) loop per property.
  • Test: per-definition specs cover single-property queries. No flow-test covers multi-property aggregation race (see Failure F6 below).
  • Pre-conditions: Step 1 + 2 completed; user's JWT carries propertyId claim for their default property, AND has access to ≥1 additional property via staffProperties cross-ref.
  • Post-conditions: Multi-property filter forwarded; each def.query runs scoped to the union; output rows carry propertyId discriminator.
  • Notes: Multi-currency hotels (e.g. ITC/Taj with USD + INR + AED properties) need explicit currency conversion. Today's revenue-monthly-report SUMs raw numbers — a USD property mixed with INR produces nonsense. [SME-PROPOSED] add def.aggregation.currencyTarget?: 'INR'|'USD' to opt-in conversion via daily FX rates from MvDailyFxRate (NEW MV).

Step 4: User selects comparative flags (vs LY / Budget / Forecast)​

  • Story: US-012B-05 [SME-PROPOSED] (per EP-012B D-012 — proposed new story) "Comparative Cells (vs LY / Budget / Forecast)" + US-012-003 D-004 [P0] "v2 revenue definitions do not actually enable comparative" — the Comparator framework (Plan 5) is shipped; per-definition enablement is partial.
  • Code: hmp-analytics-service/src/reporting/comparator/ — ReportComparator, LyEnricher, BudgetEnricher, ForecastEnricher, comparative-cell.helper.ts. Per-definition opt-in via def.comparative: {ly:{...}, budget:{metric:'revenue'}, forecast:{metric:'revenue'}}. Dashboard: CompareFlagsToggle.tsx — surfaces "Compare to: [LY | Budget | Forecast | None]". Empty-state cells carry honest reason codes (NO_LY_DATA, NO_BUDGET_UPLOADED, WARMING_UP).
  • Test: hmp-analytics-service/src/reporting/comparator/*.spec.ts — LY, Budget, Forecast empty-state reasons. Per-definition *.definition.spec.ts for the ~20 definitions that have opted in.
  • Pre-conditions: Definition has comparative slot populated. For LY: prior-year data exists in the source MV. For Budget: Budget rows uploaded via property-service. For Forecast: hmp-forecast-sidecar Prophet endpoint reachable.
  • Post-conditions: Response cells carry both current + comparative values + variance % + reason code when empty.
  • Notes: D-004 [P0] of EP-012B — revenue-daily-report + revenue-monthly-report do NOT yet declare comparative despite the spec mandating YoY (AC-EP-012B-01). One-line fix per definition. Mass-enable is Plan 3.5 scope.

Step 5: Run report → analytics-service computes via Reports v2 framework​

  • Story: US-012D-01 [SME-PROPOSED] (no catalog row) "Custom Report Query Engine (Backend)". The single execution pipeline is the load-bearing surface of the gold-standard reporting design.
  • Code: hmp-analytics-service/src/reporting/execution/report-execution.service.ts — pipeline: RBAC → audit-start → resolver → redact → drill-annotate → format → audit-complete. Two resolvers: MVResolver (calls def.query against materialized views via Prisma), LiveProxyResolver (calls source services via SERVICE_CLIENT_MAP_TOKEN). RBAC: ReportRBACService.assertCanRun checks def.rbac.roles ∩ caller.roles. Redaction: per-column def.rbac.sensitiveColumns map (e.g. police-register.passportOrIdNumber → COMPLIANCE_OFFICER only).
  • Test: report-execution.service.spec.ts ([CITED-BUT-NOT-FOUND] per EP-012D scaffolding — likely exists). Per-resolver specs. 63 × *.definition.spec.ts. Controller: reports-v2.controller.spec.ts. Cross-tenant denial template at hmp-pos-service/src/orders/__tests__/cross-tenant-denial.spec.ts is the Phase 3 rollout template — analytics-service definitions don't yet have an equivalent (EP-012D D-010 [P1]).
  • Pre-conditions: Steps 1–4 completed; assertCanRun passes.
  • Post-conditions: Report rows + columns + comparative slots + drill metadata returned. ReportRun audit row written with userId/filters/format/IP/UA/duration/rowCount (5-year retention).
  • Notes: D-008 [P0] of EP-012D — no 30-second timeout wrapper around def.query; no estimated-row-count gate. A slow query just blocks the request thread; a 500K-row query OOMs the pod. EPIC body AC-1 ("report executes within 10s for <50,000 rows; returns 429 with 'Query too large' if estimated rows > 100,000") is materially unmet. [SME-PROPOSED] wrap resolver in Promise.race([resolver(), timeoutPromise(30_000)]) → throw ReportTimeoutError → controller 504; add EXPLAIN ANALYZE row-estimate gate or hard LIMIT 100000 → 429. D-010 [P1] of EP-012D — no central enforcer guarantees every def.query includes WHERE "tenantId" = $1; a definition author who omits the filter creates a cross-tenant data leak. Add eslint AST check or ReportDefinitionsRegistrar.register compile-time guard.

Step 6: Anomalies surfaced inline (Plan 3.1 banner if HIGH/CRITICAL)​

  • Story: [SME-PROPOSED] new story — US-012B-NEW-ANOMALY-SURFACE "Inline anomaly banner on report runner". MvAnomaly ships (Plan 3.1) with dismiss audit; the dashboard banner that surfaces HIGH/CRITICAL on the per-report page is partially shipped (see notes).
  • Code: hmp-analytics-service/src/anomaly/ — MvAnomaly rows + dismiss audit (dismissedBy, dismissedAt, dismissReason). Anomaly detector consumes folio.posted + revenue.computed events; flags z-score > 3 vs trailing 30-day baseline as HIGH, > 5 as CRITICAL. Dashboard: anomaly banner component on /reports/v2/[reportId]/page.tsx (mount status — verify in code; not enumerated in EP-012B RTM).
  • Test: hmp-analytics-service/src/anomaly/*.spec.ts (anomaly detector + dismiss audit). E2E: not enumerated.
  • Pre-conditions: Report ran (Step 5); anomaly detector has at least 7 days of baseline data.
  • Post-conditions: HIGH/CRITICAL anomaly rows for the report's period surface as a sticky banner at top of the runner; one-click dismiss writes MvAnomaly.dismissedBy+dismissReason for the audit trail.
  • Notes: Failure F7 below — if the anomaly detector consumer is lagged (Redis Streams backlog), banner won't fire on stale anomalies even if MV has new HIGH rows. No SLA contract today.

Step 7: Drill-down via stackable side sheet​

  • Story: [SME-PROPOSED] new story — US-012B-NEW-DRILL-SHEET "Stackable side-sheet drill-down with breadcrumb stack + back navigation". Per-definition def.drillDown declares which dimensions drill where (e.g. revenue-daily.date → revenue-detail.date). Not in any catalog row.
  • Code: hmp-analytics-service/src/reporting/execution/report-execution.service.ts — drill-annotate step injects _drillTo metadata into row cells. Dashboard: hmp-web-dashboard/src/components/modules/reports/v2/DrillDownDrawer.tsx (Sheet primitive from shadcn). Stackable: clicking a cell in drill-1 opens drill-2 on top; back button pops the stack.
  • Test: per-definition drill specs (small). E2E: reports-v2.spec.ts covers drill-down open/close.
  • Pre-conditions: Step 5 returned rows; current definition has def.drillDown declared.
  • Post-conditions: Side sheet renders the drill target report scoped to the clicked dimension.
  • Notes: Failure F8 — infinite drill loop (definition A drills to B drills back to A) is not guarded today. [SME-PROPOSED] add cycle detection in drill-annotate step using the breadcrumb stack.

Step 8: Export request (PDF / XLSX / CSV)​

  • Story: US-012-015 (Data Export — Built-Untested per EP-012B; Plan 6 export pipeline shipped 5 formatters) + US-012D-03 (PDF Export with Hotel Letterhead — Partial) + US-012D-04 (Excel .xlsx Export — Built) + US-012B-03 (PDF Export Formatting — Partial; duplicate of D-009).
  • Code: hmp-analytics-service/src/reporting/format/csv.formatter.ts (RFC 4180, CRLF, schema-driven column order), xlsx.formatter.ts (exceljs, bold + light-grey header, col widths 16/14, sheet name 31-char cap), pdf.formatter.ts (Puppeteer + puppeteer-cluster maxConcurrency=3 + Handlebars templates; SKIP_PUPPETEER=true for CI), xml.formatter.ts (xmlbuilder2; gated to frro-c-form only), json.formatter.ts. Templates: templates/<def.id>.hbs first, then generic-tabular.hbs fallback. Controller: report-exports.controller.ts — POST /reports/export/:type, GET /reports/exports/:jobId, GET /reports/exports. Dashboard: ExportButton.tsx dropdown (PDF/XLSX/CSV items).
  • Test: csv.formatter.spec.ts, xlsx.formatter.spec.ts, pdf.formatter.spec.ts, xml.formatter.spec.ts, json.formatter.spec.ts, formatter-registry.spec.ts, report-export.service.spec.ts, report-exports.controller.spec.ts. Flow: reports-export.flow.spec.ts — currently soft-skip (tripwire; flips when POST /api/reports/v2/:id/run?format=pdf is asserted).
  • Pre-conditions: Step 5 completed; user has READ permission on the report (RBAC inherited).
  • Post-conditions: Sync path (≤500 rows) returns the artifact inline; async path queues a BullMQ job with state in Redis (report-exports:{tenantId}:{jobId}, 24-h TTL) and returns {jobId} for polling.
  • Notes: D-008 [P0] of EP-012B — review-file verdict FAIL on US-012-006 is stale; all 12 review-GAPs closed in Plan 6. D-009 [P1] of EP-012B — no per-report .hbs templates; all 63 definitions fall through to generic-tabular.hbs; AC-EP-012B-05 (hotel-letterhead PDF) unmet. [SME-PROPOSED] author branded-tabular.hbs pulling property.logoUrl/gstin/name/address/phone from PropertyServiceClient.getProperty(propertyId) (5-min cache); enable formatINR(value) Handlebars helper → ₹ + Intl.NumberFormat('en-IN').format(value) for lakhs/crores grouping (₹48,75,000); <thead> with display: table-header-group so headers repeat on every page. CSV: UTF-8 BOM (\xEF\xBB\xBF) for Hindi/Devanagari Excel compat. XLSX: per-locale numFmt '##,##,##0.00' (Indian grouping).

Step 9: Async generation (BullMQ scheduler) → S3 presigned URL​

  • Story: same as Step 8 (US-012-015) + US-012D-05 [SME-PROPOSED] (Report Run History Log).
  • Code: hmp-analytics-service/src/reporting/exports/report-export.service.ts — sync ≤500 rows; queued via BullMQ otherwise; Redis-backed job state; S3 presigned URL when AWS_REPORT_EXPORTS_BUCKET set, else base64 data-URL. exports/storage/S3ExportStorage (Plan 6, @Optional) — generates 24-h presigned GET URLs. Job persists ReportRun row on completion (userId/filters/format/IP/UA/duration/rowCount/presigned URL).
  • Test: report-export.service.spec.ts covers sync vs async branching + presigned URL.
  • Pre-conditions: Step 8 returned a jobId; AWS_REPORT_EXPORTS_BUCKET configured (else falls back to base64 inline).
  • Post-conditions: Job state transitions QUEUED → RUNNING → SUCCEEDED with a presigned S3 URL; ReportRun.exportUrl carries the link.
  • Notes: Failure F4 below — export OOM on >100K-row reports (no row-estimate gate in Step 5 yet). No retry on transient S3 PUT failures today; one-shot.

Step 10: (Scheduled mode) Save view → schedule cron → email delivery via SES​

  • Story: US-012-20 (Scheduled Report Delivery — Partial per EP-012D) + US-012B-06 [SME-PROPOSED] "Scheduled Report Distribution (Email/WhatsApp/S3/SFTP)" + US-012B-04 [SME-PROPOSED] "Saved Views with Pin/Share". Catalog row US-012-015 Report Scheduling (Auto-Email) marked Not Started but backend is shipped — see EP-012D D-005.
  • Code: Saved view: saved-views.controller.ts — POST/GET/GET:id/PATCH/DELETE /reports/v2/saved-views + POST :id/schedule. Schedule: scheduler/report-scheduler.service.ts (BullMQ recurring; loads all active schedules at boot; dedupe key = scheduleId; SKIP_REPORT_SCHEDULER=true opt-out). Distribution: distribution/distribution.service.ts (aggregator → 4 channels in parallel; partial-failure capture) + EmailChannel (POST /notifications/email/send via NotificationHttpClient — backed by AWS SES via IRSA per OP-1/OP-2; SendGrid retired 2026-05-21) + WhatsappChannel (whitelist: manager-flash-report, revenue-daily-report, arrival-report, departure-report only — 24-h WhatsApp Business window + template approval gate) + S3Channel + SftpChannel. Distribution lists: distribution-lists.controller.ts — 5 endpoints CRUD. Dashboard schedule entry: reports/v2/saved/_components/ScheduleSavedViewDialog.tsx (per-saved-view only — no stand-alone /reports/scheduled page exists; EP-012D D-005).
  • Test: saved-views.controller.spec.ts, saved-views.service.spec.ts (RBAC: ADMIN+ for shared=true), scheduler/*.spec.ts, distribution/*.spec.ts (incl. UnsupportedChannelError), distribution-lists/*.spec.ts. Flow: saved-views-schedules.flow.spec.ts — 8 scenarios end-to-end (private create → list → patch pin → filter pinned → shared create (admin) → convert to schedule → list schedules → delete). EP-012D D-002 [P1]: flow exists but NOT enumerated in 2026-05-16-13-flow-demo-readiness.md — operators can't tell pass-rate.
  • Pre-conditions: Step 5+ executed at least once with desired filters; user saved the view (POST /saved-views); user has email recipients available (or DistributionList.members populated); SES sender identity verified for mail.<env>.keynova.ai (OP-3 — manual sandbox-removal pending in prod per hmp-infrastructure CLAUDE.md).
  • Post-conditions: ReportSchedule row persisted with (reportId, cron, timezone=Asia/Kolkata, channels JSON, filters JSON, active=true). BullMQ recurring job registered. On every fire: render report → format → distribute → write ReportRun row.
  • Notes: D-004 [P0] of EP-012D — EPIC AC-2 "retry once after 15 minutes, then send failure alert to admin" is materially unmet. ReportSchedulerService increments failureCount + lastFailureMsg on error but does NOT enqueue a delayed retry; BullMQ defaults to attempts: 1. P&L delivery will silently miss on transient Redis/SES blip. [SME-PROPOSED] set BullMQ defaultJobOptions: {attempts: 2, backoff: {type:'fixed', delay: 15*60*1000}} + failed event handler that emits report.scheduled.failed Kafka topic on second failure → notification-service sends admin alert. D-007 [P1] of EP-012D — cron field accepted without validation; invalid cron silently fails first job; add class-validator + cron-parser at DTO boundary. India scheduling reality: FY runs April→March, but cron is calendar-based; [SME-PROPOSED] add def.scheduleAliases: {'fy-month-1': '0 8 1 4 *', 'fy-quarter-1': ..., ...} so an operator picks "1st of FY Q1" not '0 8 1 4 *'.

Step 11: Audit log entry (5-year retention)​

  • Story: US-012-14 (Audit Log with Integrity Chain — catalog claims Built; reality Partial — no hash chain per EP-012C D-008 [P0]) + US-012D-05 [SME-PROPOSED] (Report Run History Log — Partial; backend trail exists, no UI).
  • Code: hmp-analytics-service/src/reporting/audit/report-audit.service.ts writes ReportRun rows on every v2 report execution AND every scheduled fire (userId, filters, format, IP, UA, duration, rowCount, presigned export URL). Schema ReportRun (Prisma) — 5-year retention per CERT-In + GST audit. Controller: reports-v2.controller.ts :: GET /reports/v2/:id/runs paginated list. Anomaly trail: MvAnomaly dismiss audit (separate concern). Hash-chain AuditLog entity does NOT exist anywhere in code.
  • Test: ReportAuditService coverage implicit in reporting/ tests.
  • Pre-conditions: Step 5 or Step 10 fire executed.
  • Post-conditions: ReportRun row persisted. Retention enforced at DB level (5y partition window).
  • Notes: D-008 [P0] of EP-012C — Spec EP-012C lines 73-74 + 87 + 175-198 describe AuditLog { id, entryHash, previousHash, eventType, payload, createdAt } with SHA-256 chain + GET /auditlog/verify returning first tampered entry. None of this exists. Catalog says Built; reality says Stub. Tenant facing a CERT-In or RBI audit cannot answer "is this trail tamper-evident" today. [SME-PROPOSED] extend EventStore with entryHash String? + previousHash String? columns; daily cron backfills createdAt order computing SHA-256(previousHash || eventType || payload || createdAt); expose GET /auditlog/verify?fromDate=&toDate=&propertyId= returning first tampered entry ID + chain-break range, RBAC COMPLIANCE_OFFICER | SUPER_ADMIN | ADMIN. Until shipped, explicitly disclaim in the dashboard compliance page that audit-log integrity is via ReportRun (per-execution, not per-financial-event). D-005 [P0] of EP-012D — /reports/scheduled UI doesn't exist; operators can't see "did my Monday morning report fire". Same GET /reports/v2/:id/runs serves both manual + scheduled runs (a fire writes a ReportRun row identically).

Step 12: (Compliance) For GSTR-1: validate against NIC schema v3.4, export ready for GSTN portal upload​

  • Story: US-012-04 (GSTR-1 — Partial per EP-012C) + US-012-05 (GSTR-3B — Partial) + US-012C-01 (CDNR — Stub) + US-012C-06 (IGST Inter-State — Partial).
  • Code: Two parallel implementations exist (EP-012C D-003 [P0]):
    • Frontdesk path: hmp-frontdesk-service/src/compliance/gstr-reports.controller.ts — GET /compliance/gstr1, GET /compliance/gstr3b (manager+/accountant). Service: gstr-reports.service.ts::Gstr1ExportService.generateGstr1() — live-folio scan, take: 1000 cap, single-month.
    • Analytics path: hmp-analytics-service/src/gstr/gstr.controller.ts:43-100 — GET /reports/gstr3b?month=YYYY-MM&propertyId=UUID&forceRefresh=true (cache-first via MvGstr3bSnapshot, AP-T10-005). Service: GstrService.computeGstr3b() aggregates folio.posted events from EventStore. v2 report: reporting/reports/gst.definition.ts (composite over MvGstr3bSnapshot + MvEInvoiceRegister, formats json/csv/pdf/xlsx).
    • IRN generation (per-folio at checkout): hmp-frontdesk-service/src/einvoice/einvoice.service.ts — queue-routed via EInvoiceJob; B2B path AP-T08-003 (company GSTIN drives buyerDetails.stateCode → inter-state IGST); B2C path uses property.state.substring(0, 2) (supplier state) — WRONG, should use guest billing state (place-of-supply).
    • CDNR: CreditNote model + generateCreditNote(folioId, originalIrn, propertyId) (AP-T08-007) — queue-routed documentType='CRN'; IRN persisted to CreditNote.creditNoteIrn. GSTR-1 Section 9 export does NOT read these rows (EP-012C D-009 [P0]).
  • Test: hmp-analytics-service/src/gstr/gstr.controller.spec.ts (controller specs only — no direct gstr.service.spec.ts for computeGstr3b() arithmetic, EP-012C D-007 [P1]). hmp-frontdesk-service/src/folios/__tests__/gsa-067-igst-split.spec.ts (IGST vs CGST+SGST). hmp-frontdesk-service/src/folios/consumers/pos-folio-charge.consumer.spec.ts (29 specs incl. 9 AP-T07-004 cases — explicit-split honour / derive intra-vs-inter / supplier-state fallback / outbox envelope). hmp-frontdesk-service/src/folios/fu-55-folio-event-dual-emit.spec.ts (8 specs covering canonical FOLIO_POSTED payload). Flow: gst-tax-flags.flow.spec.ts :: P0-7 — /compliance/gstr1 + /compliance/gstr3b respond with rows + totals (soft-skip if 404).
  • Pre-conditions: Period defines a calendar month (GSTR-1 + GSTR-3B are month-bounded); tenant has GSTIN populated; non-demo tenant (auto-IRN gated by isDemoTenant=false).
  • Post-conditions: GSTR-1 JSON output ready for portal upload (B2B / B2C / Section 9 CDNR / HSN summary blocks). GSTR-3B JSON with B2B+B2C output tax liability + HSN breakdown. ITC section hardcoded to 0 with explanatory note (EP-012C D-006 [P0]).
  • Notes: D-001 [P0] of EP-012C — Catalog (US-012-005 single row) vs spec (US-012-04 + US-012-05 + US-012-14 + US-012C-01..06 9 stories) disagree on numbering. D-002 [P0] — Task brief cites uc-gstr3b-prep.yaml + uc-frontdesk-einvoice-irn.yaml + uc-frontdesk-einvoice-b2b.yaml — none exist. D-003 [P0] — Two GSTR-1 implementations drift; retire Gstr1ExportService in frontdesk; route /compliance/gstr1 through gateway to analytics-service gst-report v2. D-004 [P0] — No NIC schema v3.4 validation anywhere in code. AC-1 "JSON output passes schema validation against the GST government schema v3.4" has zero implementation. Portal rejection on first upload is the platform's stated north-star failure mode. [SME-PROPOSED] validate via ajv at the boundary with schema files in @hmp/common/src/gst-schemas/; CI gate on 50 synthetic + 5 anonymized fixtures per release. D-005 [P1] — NicEInvoiceAdapter lacks Retry-After header handling, no exponential backoff on 429, no per-GSTIN concurrency cap. A night-audit run of 80 IRNs in 60s can saturate the 1000-IRN/sec sustained limit. D-006 [P0] — GSTR-3B ITC=0 hardcoded; net tax payable understates; filing verbatim overpays. [SME-PROPOSED] metadata flag itcStubbed: true + dashboard confirmation modal + manual override entity Gstr3bItcOverride. D-009 [P0] — CDNR IRNs minted but never exported to GSTR-1 Section 9. D-011 [P1] — B2C inter-state IGST path missing (Maharashtra→Karnataka guest emits CGST+SGST instead of IGST).
    • GST filing deadlines: GSTR-1 by 11th of following month, GSTR-3B by 20th. Late: ₹50/day + ₹50/day for nil returns up to ₹5,000 cap per return; interest 18% p.a. on unpaid tax. Compliance Calendar (US-012C-04) is Not Started — no in-platform reminder.
    • TDS Section 194-O (OTA commission): platform owes 1% TDS on payments to OTAs since 1-Oct-2020. Not in any current code path; relevant when modelling channel-commission payouts back to OTAs (US-012B-01 + US-012-013).
    • Indian FY: April–March for ALL tax filings; calendar-year output of revenue reports needs fy alias support (see Step 2).
    • Devanagari PDF rendering: NOT verified today; depends on whether the Handlebars template's font stack includes a Devanagari fallback. Required for Hindi-language guest invoices in Tier-2/3 markets.

Failure modes worth testing​

#ScenarioExpected behaviourCoverage todayOwning story
F1Report query takes >30s (no timeout gate)504 Gateway Timeout with {code:'REPORT_TIMEOUT', timeoutMs:30000}; request thread releasedNONE — def.query blocks indefinitely (EP-012D D-008 [P0])US-012D-01 [SME-PROPOSED]
F2Schedule fails first time (transient Redis blip); BullMQ does not retryAuto-retry after 15 min; on 2nd failure, fire report.scheduled.failed Kafka event → admin email alertNONE — attempts: 1 default; failureCount++ only (EP-012D D-004 [P0])US-012-20
F3GSTR-1 JSON rejected by NIC portal on upload (schema mismatch v3.4)Pre-upload validation via ajv against bundled v3.4 schema; reject locally with field-level errors; ops alert via Slack/TeamsNONE — no schema validator anywhere (EP-012C D-004 [P0])US-012-04
F4Export job OOM on 500K-row reportPre-flight row-estimate via EXPLAIN ANALYZE; reject with 429 + {code:'REPORT_TOO_LARGE', estimatedRows, max:100000} BEFORE format stepNONE — no row-estimate gate (EP-012D D-008 [P0] same fix)US-012D-01 + US-012-015
F5Scheduled email bounces (recipient mailbox full)SES bounce notification → notification-service updates DistributionList.member.lastBounceAt; admin sees yellow-banner on /reports/scheduled after N=3 consecutive bouncesNONE — no bounce handler wired; SES bounce SNS topic exists per OP-1 module but no analytics-side consumerUS-012-20 [SME-PROPOSED]
F6Multi-property aggregation race — property B's MV refresh runs mid-query; report sums stale-from-A + fresh-from-BAtomic snapshot read via BEGIN ISOLATION LEVEL REPEATABLE READ around all per-property queries; OR document the staleness window (≤2 min per Plan 5 enrichers) in the runner UINONE — no transaction wrapping, no docs[SME-PROPOSED] US-012D-NEW-MULTI-PROP
F7Anomaly detector consumer lagged; MV has new HIGH/CRITICAL row but banner doesn't fireBanner subscribes via WebSocket OR re-fetches anomalies on report run; SLA contract published (e.g. "anomaly latency P99 < 5 min")NONE — banner reads MV at runner mount only[SME-PROPOSED] US-012B-NEW-ANOMALY-SURFACE
F8Drill-down infinite loop (A → B → A)Cycle detection in drill-annotate using breadcrumb stack; reject with DRILL_CYCLE_DETECTED 400NONE — no cycle guard[SME-PROPOSED] US-012B-NEW-DRILL-SHEET
F9Cross-tenant data leak via forgotten WHERE "tenantId" in def.queryESLint AST rule fails CI build; ReportDefinitionsRegistrar.register boot-time check throwsNONE — no enforcement (EP-012D D-010 [P1])US-012D-01
F10Hindi (Devanagari) folio printed via PDF formatter — characters render as boxesHandlebars template font stack includes Noto Sans Devanagari; Puppeteer launched with --font-render-hinting=none; smoke test renders fixture invoice with Hindi guest nameNONE — not verifiedUS-012B-03 / US-012D-03
F11Tenant on free plan tries to schedule reports (gated to PROFESSIONAL/ENTERPRISE)402 Payment Required with {code:'FEATURE_GATED_REPORT_SCHEDULER', requiredPlan:'PROFESSIONAL'}UNVERIFIED — ReportSchedulerService does not check plan; auth-service PlanFeatureGuard not applied[SME-PROPOSED] US-012-20 plan-gate

Drift summary​

The 12 BF-012-surfaced drifts that downstream Task 6 SME passes must adjudicate (in addition to the 12+11+11 drifts already authored in the per-EPIC RTMs). P0 = blocks Property #1 launch readiness; P1 = ships next sprint; P2 = backlog.

IDTypeSeverityDescriptionSurfaced by stepCross-ref
BF-012-D-001Cross-EPIC numberingP0EP-012B/C/D numbering vs catalog: 3 sub-EPICs each carry their own US-NNN ids that don't appear in 01-epic-catalog.md; 4 saved-views/comparator/scheduler/pinned stories are orphan code with no story. New PMs allocate duplicate work.Steps 1, 4, 10, 11EP-012B D-010+D-012; EP-012C D-001; EP-012D D-001
BF-012-D-002India-fiscal-year-vs-codeP1filters.period ships today/yesterday/mtd/ytd/custom only — calendar-year semantics. Indian Finance Managers running "YTD revenue" in October expect April–October, get Jan–October. No fy/fytd/fq aliases.Step 2NEW
BF-012-D-003Cross-property aggregationP1Multi-property roll-ups SUM raw numbers across currencies (e.g. USD property + INR property = nonsense). No def.aggregation.currencyTarget opt-in.Step 3NEW
BF-012-D-004Resolver timeout + OOMP0No 30-s timeout wrapper around def.query; no estimated-row-count gate. EPIC AC-1 (10s/<50K rows; 429 at >100K) materially unmet. Slow query blocks thread; 500K-row query OOMs pod.Steps 5, 9EP-012D D-008
BF-012-D-005Cross-tenant leak riskP1No central enforcer guarantees every def.query includes WHERE "tenantId"=$1. ESLint AST check absent. 63 definitions × 1 author-forgets = full-tenant leak.Step 5EP-012D D-010
BF-012-D-006Scheduler retry missingP0EPIC AC-2 "retry once after 15 min, then admin alert" unmet. BullMQ attempts: 1; transient blip → silent miss of monthly P&L delivery.Step 10EP-012D D-004
BF-012-D-007Cron validation missingP1ScheduleSavedViewDto.cron accepted without syntax check. Invalid cron persists; silent first-fire failure.Step 10EP-012D D-007
BF-012-D-008Stand-alone scheduled-reports UI missingP0Per-saved-view dialog only; no /reports/scheduled page. Operators can't answer "did my Monday report fire?" without SSH+SQL.Step 10EP-012D D-005
BF-012-D-009NIC schema v3.4 validation absentP0"JSON output passes schema validation against the GST government schema v3.4" AC has zero implementation. Portal-rejection is the platform's north-star failure mode for compliance.Step 12EP-012C D-004
BF-012-D-010GSTR-3B ITC always 0P0Net tax payable understates by full ITC amount; filing verbatim overpays. No itcStubbed flag, no manual override entity.Step 12EP-012C D-006
BF-012-D-011Hash-chain audit log absentP0Catalog + spec claim AuditLog { entryHash, previousHash } + GET /auditlog/verify. None of it exists. CERT-In + RBI audit posture is theatrical, not real.Step 11EP-012C D-008
BF-012-D-012CDNR not exported to GSTR-1 Section 9P0CreditNote.creditNoteIrn minted end-to-end (queue + idempotency + DLQ) but GSTR-1 export never reads them. Guest tax liability stays inflated, hotel outward-supply totals stay inflated.Step 12EP-012C D-009

P0 count: 8. Top P0 is BF-012-D-009 (NIC schema v3.4 validation absent) — Property #1 cannot file GSTR-1 from HMP without a manual JSON-Schema check in Excel until this lands. Closely followed by BF-012-D-006 (silent missed scheduled deliveries) and BF-012-D-011 (audit-chain claim that cannot be backed up under regulator scrutiny).


Cross-EPIC narrative summary​

StepSpansWhy cross-EPIC
1 → 4EP-012B (catalog browse + period + comparative) ↔ EP-012D (role-filtered catalog)Catalog discovery is owned by D; per-report definitions are owned by B/C.
5 → 6EP-012B (definitions) ↔ EP-012D (execution pipeline) ↔ EP-012B (anomaly MV)Execution + RBAC + redaction live in D's framework; per-definition SQL lives in B/C; anomaly surface ties back to B.
7EP-012B (drill targets) ↔ EP-012D (drill plumbing)Drill cells annotated by D's drill-annotate step; targets are B's def.drillDown declarations.
8 → 9EP-012D (formatters + export queue) ↔ EP-012B (PDF letterhead spec)Formatters in D; per-report PDF templates in B's US-012B-03 (Hotel Letterhead).
10EP-012D (scheduler + distribution) ↔ EP-012B (saved view CRUD) ↔ hmp-notification-service (SES via IRSA per OP-1/OP-2)3-service handoff: schedule fires in analytics → render via execution pipeline → distribute via EmailChannel → notification-service authenticates to AWS SES via IRSA.
11EP-012D (ReportRun audit) ↔ EP-012C (hash-chain AuditLog claim)The "audit log" surface has TWO non-overlapping pieces of truth: D's ReportRun (per-execution trail, exists) and C's AuditLog (per-financial-event hash chain, does not exist). Catalog conflates them.
12EP-012C (GSTR-1/3B + CDNR + IGST) ↔ hmp-frontdesk-service (folio + e-invoice IRN) ↔ hmp-analytics-service (gst-report v2)Two parallel GSTR-1 implementations (frontdesk vs analytics). IRN minted in frontdesk; aggregated in analytics; consumed by both /compliance/gstr1 and /reports/gstr3b. Duplicate-impl drift is the load-bearing P0 of EP-012C.

The five cross-EPIC seams worth narrating to the SME:

  1. Catalog discovery in D vs definitions in B/C (Step 1) — fixing role-filtered catalog in D auto-improves UX for every B/C report.
  2. Execution pipeline in D vs SQL in B/C (Step 5) — timeout/row-gate fix in D shields every definition in B/C without per-definition change.
  3. Scheduled delivery spans 3 services (Step 10) — analytics → notification → SES; failure in any one is opaque to the operator today.
  4. Audit log is two non-overlapping things (Step 11) — ReportRun is real; hash-chain AuditLog is fiction. Catalog must distinguish.
  5. GSTR-1 has two implementations (Step 12) — retire frontdesk path; route gateway → analytics gst-report v2; close 4 P0s in one architectural move.

Day-2 reporting (first 30 days, NOT in pilot scope)​

Items the Finance Manager / GM typically tackles in the first 30 days post go-live. Not in BF-012 scope but flagged so future BFs know where to pick up:

  • Compliance Calendar bootstrap (US-012C-04 — Not Started) — GSTR-1 11th, GSTR-3B 20th, TDS 7th, TCS 10th, IRN 24-h cancel window, professional tax, ESIC, EPF.
  • Regional Tax configuration (US-012C-03 — Not Started) — Goa tourism levy 5%, Maharashtra luxury tax >₹7,500, Rajasthan tourism development fee. P0 for any non-Karnataka/Delhi/UP launch.
  • STR Destination Report opt-in (US-012C-05 — Not Started, P3) — only for hotels under Marriott/IHG/Hyatt brand contracts.
  • F&B Cost Analysis (COGS + Contribution Margin) (US-012B-02 — Partial) — blocked by Inventory module (US-012B-02 D-011 → proposed EP-007B Inventory & Recipe Management).
  • P&L expense ingestion (US-012-009 — Partial) — blocked by property-service Plan 11 expense module wiring + USALI default chart-of-accounts (EP-012B D-005 [P0]).
  • Balance Sheet + Cash Flow (US-012-010 — Stub) — blocked by Tally / Zoho Books integration (EP-020 Out of Scope).
  • Role-override grants (EP-012D D-011 [P2]) — ReportRoleOverride model to grant housekeeping-productivity to a custom OPS_LEAD role without redeploying.
  • Saved-view role-scoped sharing (US-012D-06) — replace shared: boolean with visibility: 'PRIVATE' | 'SHARED_ROLES' | 'SHARED_TENANT' + sharedRoles?: string[].
  • Devanagari + Tamil + Telugu PDF rendering (NEW) — multi-lingual guest invoices for Tier-2/3 markets; verify font stack + Puppeteer render.
  • SES production-access removal (OP-3 per hmp-infrastructure CLAUDE.md) — mandatory before real tenant email flows; test/qa/staging stay in sandbox.
  • GSTR-2A reconciliation (NEW — not in any spec) — Indian Finance Managers manually reconcile inward supplies against vendor's GSTR-1 filings; quarterly task; CA usually owns it.
  • Form 26AS quarterly download (NEW) — TDS deducted by hotels' counterparties; visible only in Form 26AS via the IT portal; reconcile against book TDS.

See also​