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
| Persona | Role | Source |
|---|---|---|
| Finance Manager | Owns GSTR-1, GSTR-3B, P&L, F&B cost analysis; files monthly returns by 11th / 20th | EP-012C spec body §1 + 01-epic-catalog.md EP-012 surface |
| GM / Owner | Daily ops reports, anomaly review, pinned widget on home dashboard | EP-012B spec body + pinned-reports-widget.tsx |
| Accountant / CA | Quarterly Form 26AS, year-end audit, ITC manual override on GSTR-3B | EP-012C spec body §10 + US-012-005 review |
| Revenue Manager | Channel commission, market segment, source-wise revenue + LY/Budget/Forecast comparator | EP-012B + Plan 5 Comparator |
| Compliance Officer | Police register, FRRO C-Form, audit-log integrity verify | EP-012C US-012-14 + police-register definition |
| SUPER_ADMIN | Cross-tenant viewing (for support), role-override grants for custom roles | EP-012D US-012D-02 + ReportRBACService |
Entry points
| Entry | URL / API | Backed by |
|---|---|---|
| Reports v2 catalog (browse all 63 reports) | https://dashboard.<env>.keynova.ai/reports/v2 | hmp-web-dashboard/src/app/(dashboard)/reports/v2/page.tsx → GET /api/reports/v2/catalog |
| Per-report runner | https://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 list | https://dashboard.<env>.keynova.ai/reports/v2/saved | Plan 3.4 — saved-views.controller.ts |
| Pinned home widget | https://dashboard.<env>.keynova.ai/ | PinnedReportsWidget.tsx reading saved views pinnedToHome=true |
| Compliance page (legacy GSTR routes) | https://dashboard.<env>.keynova.ai/settings/compliance | settings/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/:type | reports-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/catalogreturns the 63 pre-registeredReportDefinitionrows from the in-memoryReportRegistry(registered at boot byReportDefinitionsRegistrar). 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-reportand 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 genericvalidatePeriodguard atreports-v2.controller.ts:18-41(added 2026-05-16, rejects malformedfilters.periodwith structured 400) and thePeriodSelectorUI component. Catalog has no row for fiscal-year semantics. - Code:
hmp-analytics-service/src/reporting/controller/reports-v2.controller.ts:18-41—validatePeriodguard. 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.tscoversvalidatePeriod. E2E:reports-v2.spec.tsexercises period change. - Pre-conditions: Step 1 completed; user landed on
/reports/v2/[reportId]. - Post-conditions:
filters.periodpayload 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
ytdshape 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]addtype:'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 byReportExecutionServicewhich forwards to eachdef.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
propertyIdclaim for their default property, AND has access to ≥1 additional property viastaffPropertiescross-ref. - Post-conditions: Multi-property filter forwarded; each
def.queryruns scoped to the union; output rows carrypropertyIddiscriminator. - Notes: Multi-currency hotels (e.g. ITC/Taj with USD + INR + AED properties) need explicit currency conversion. Today's
revenue-monthly-reportSUMs raw numbers — a USD property mixed with INR produces nonsense.[SME-PROPOSED]adddef.aggregation.currencyTarget?: 'INR'|'USD'to opt-in conversion via daily FX rates fromMvDailyFxRate(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 viadef.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.tsfor the ~20 definitions that have opted in. - Pre-conditions: Definition has
comparativeslot populated. For LY: prior-year data exists in the source MV. For Budget:Budgetrows uploaded via property-service. For Forecast:hmp-forecast-sidecarProphet 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-reportdo NOT yet declarecomparativedespite 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(callsdef.queryagainst materialized views via Prisma),LiveProxyResolver(calls source services viaSERVICE_CLIENT_MAP_TOKEN). RBAC:ReportRBACService.assertCanRunchecksdef.rbac.roles ∩ caller.roles. Redaction: per-columndef.rbac.sensitiveColumnsmap (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 athmp-pos-service/src/orders/__tests__/cross-tenant-denial.spec.tsis 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;
assertCanRunpasses. - Post-conditions: Report rows + columns + comparative slots + drill metadata returned.
ReportRunaudit 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 inPromise.race([resolver(), timeoutPromise(30_000)])→ throwReportTimeoutError→ controller 504; addEXPLAIN ANALYZErow-estimate gate or hardLIMIT 100000→ 429. D-010 [P1] of EP-012D — no central enforcer guarantees everydef.queryincludesWHERE "tenantId" = $1; a definition author who omits the filter creates a cross-tenant data leak. Add eslint AST check orReportDefinitionsRegistrar.registercompile-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".MvAnomalyships (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/—MvAnomalyrows + dismiss audit (dismissedBy,dismissedAt,dismissReason). Anomaly detector consumesfolio.posted+revenue.computedevents; 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+dismissReasonfor 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-definitiondef.drillDowndeclares 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-annotatestep injects_drillTometadata 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.tscovers drill-down open/close. - Pre-conditions: Step 5 returned rows; current definition has
def.drillDowndeclared. - 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 indrill-annotatestep 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=truefor CI),xml.formatter.ts(xmlbuilder2; gated tofrro-c-formonly),json.formatter.ts. Templates:templates/<def.id>.hbsfirst, thengeneric-tabular.hbsfallback. Controller:report-exports.controller.ts—POST /reports/export/:type,GET /reports/exports/:jobId,GET /reports/exports. Dashboard:ExportButton.tsxdropdown (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 whenPOST /api/reports/v2/:id/run?format=pdfis asserted). - Pre-conditions: Step 5 completed; user has
READpermission on the report (RBAC inherited). - Post-conditions: Sync path (≤500 rows) returns the artifact inline; async path queues a
BullMQjob 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
.hbstemplates; all 63 definitions fall through togeneric-tabular.hbs; AC-EP-012B-05 (hotel-letterhead PDF) unmet.[SME-PROPOSED]authorbranded-tabular.hbspullingproperty.logoUrl/gstin/name/address/phonefromPropertyServiceClient.getProperty(propertyId)(5-min cache); enableformatINR(value)Handlebars helper →₹+Intl.NumberFormat('en-IN').format(value)for lakhs/crores grouping (₹48,75,000);<thead>withdisplay: table-header-groupso 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 whenAWS_REPORT_EXPORTS_BUCKETset, else base64 data-URL.exports/storage/S3ExportStorage(Plan 6,@Optional) — generates 24-h presigned GET URLs. Job persistsReportRunrow on completion (userId/filters/format/IP/UA/duration/rowCount/presigned URL). - Test:
report-export.service.spec.tscovers sync vs async branching + presigned URL. - Pre-conditions: Step 8 returned a
jobId;AWS_REPORT_EXPORTS_BUCKETconfigured (else falls back to base64 inline). - Post-conditions: Job state transitions QUEUED → RUNNING → SUCCEEDED with a presigned S3 URL;
ReportRun.exportUrlcarries 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 rowUS-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 allactiveschedules at boot; dedupe key =scheduleId;SKIP_REPORT_SCHEDULER=trueopt-out). Distribution:distribution/distribution.service.ts(aggregator → 4 channels in parallel; partial-failure capture) +EmailChannel(POST/notifications/email/sendviaNotificationHttpClient— 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-reportonly — 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/scheduledpage exists; EP-012D D-005). - Test:
saved-views.controller.spec.ts,saved-views.service.spec.ts(RBAC: ADMIN+ forshared=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 in2026-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 (orDistributionList.memberspopulated); SES sender identity verified formail.<env>.keynova.ai(OP-3 — manual sandbox-removal pending in prod per hmp-infrastructure CLAUDE.md). - Post-conditions:
ReportSchedulerow persisted with(reportId, cron, timezone=Asia/Kolkata, channels JSON, filters JSON, active=true). BullMQ recurring job registered. On every fire: render report → format → distribute → writeReportRunrow. - Notes: D-004 [P0] of EP-012D — EPIC AC-2 "retry once after 15 minutes, then send failure alert to admin" is materially unmet.
ReportSchedulerServiceincrementsfailureCount+lastFailureMsgon error but does NOT enqueue a delayed retry; BullMQ defaults toattempts: 1. P&L delivery will silently miss on transient Redis/SES blip.[SME-PROPOSED]set BullMQdefaultJobOptions: {attempts: 2, backoff: {type:'fixed', delay: 15*60*1000}}+failedevent handler that emitsreport.scheduled.failedKafka topic on second failure → notification-service sends admin alert. D-007 [P1] of EP-012D —cronfield accepted without validation; invalid cron silently fails first job; addclass-validator+cron-parserat DTO boundary. India scheduling reality: FY runs April→March, butcronis calendar-based;[SME-PROPOSED]adddef.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.tswritesReportRunrows on every v2 report execution AND every scheduled fire (userId, filters, format, IP, UA, duration, rowCount, presigned export URL). SchemaReportRun(Prisma) — 5-year retention per CERT-In + GST audit. Controller:reports-v2.controller.ts::GET /reports/v2/:id/runspaginated list. Anomaly trail:MvAnomalydismiss audit (separate concern). Hash-chainAuditLogentity does NOT exist anywhere in code. - Test:
ReportAuditServicecoverage implicit inreporting/tests. - Pre-conditions: Step 5 or Step 10 fire executed.
- Post-conditions:
ReportRunrow 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/verifyreturning 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]extendEventStorewithentryHash String?+previousHash String?columns; daily cron backfillscreatedAtorder computingSHA-256(previousHash || eventType || payload || createdAt); exposeGET /auditlog/verify?fromDate=&toDate=&propertyId=returning first tampered entry ID + chain-break range, RBACCOMPLIANCE_OFFICER | SUPER_ADMIN | ADMIN. Until shipped, explicitly disclaim in the dashboard compliance page that audit-log integrity is viaReportRun(per-execution, not per-financial-event). D-005 [P0] of EP-012D —/reports/scheduledUI doesn't exist; operators can't see "did my Monday morning report fire". SameGET /reports/v2/:id/runsserves both manual + scheduled runs (a fire writes aReportRunrow 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: 1000cap, 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 viaMvGstr3bSnapshot, AP-T10-005). Service:GstrService.computeGstr3b()aggregatesfolio.postedevents fromEventStore. v2 report:reporting/reports/gst.definition.ts(composite overMvGstr3bSnapshot + MvEInvoiceRegister, formatsjson/csv/pdf/xlsx). - IRN generation (per-folio at checkout):
hmp-frontdesk-service/src/einvoice/einvoice.service.ts— queue-routed viaEInvoiceJob; B2B path AP-T08-003 (company GSTIN drives buyerDetails.stateCode → inter-state IGST); B2C path usesproperty.state.substring(0, 2)(supplier state) — WRONG, should use guest billing state (place-of-supply). - CDNR:
CreditNotemodel +generateCreditNote(folioId, originalIrn, propertyId)(AP-T08-007) — queue-routeddocumentType='CRN'; IRN persisted toCreditNote.creditNoteIrn. GSTR-1 Section 9 export does NOT read these rows (EP-012C D-009 [P0]).
- Frontdesk path:
- Test:
hmp-analytics-service/src/gstr/gstr.controller.spec.ts(controller specs only — no directgstr.service.spec.tsforcomputeGstr3b()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 canonicalFOLIO_POSTEDpayload). Flow:gst-tax-flags.flow.spec.ts:: P0-7 —/compliance/gstr1+/compliance/gstr3brespond 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-005single row) vs spec (US-012-04 + US-012-05 + US-012-14 + US-012C-01..069 stories) disagree on numbering. D-002 [P0] — Task brief citesuc-gstr3b-prep.yaml+uc-frontdesk-einvoice-irn.yaml+uc-frontdesk-einvoice-b2b.yaml— none exist. D-003 [P0] — Two GSTR-1 implementations drift; retireGstr1ExportServicein frontdesk; route/compliance/gstr1through gateway to analytics-servicegst-reportv2. 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 viaajvat the boundary with schema files in@hmp/common/src/gst-schemas/; CI gate on 50 synthetic + 5 anonymized fixtures per release. D-005 [P1] —NicEInvoiceAdapterlacksRetry-Afterheader 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 flagitcStubbed: true+ dashboard confirmation modal + manual override entityGstr3bItcOverride. 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
fyalias 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
| # | Scenario | Expected behaviour | Coverage today | Owning story |
|---|---|---|---|---|
| F1 | Report query takes >30s (no timeout gate) | 504 Gateway Timeout with {code:'REPORT_TIMEOUT', timeoutMs:30000}; request thread released | NONE — def.query blocks indefinitely (EP-012D D-008 [P0]) | US-012D-01 [SME-PROPOSED] |
| F2 | Schedule fails first time (transient Redis blip); BullMQ does not retry | Auto-retry after 15 min; on 2nd failure, fire report.scheduled.failed Kafka event → admin email alert | NONE — attempts: 1 default; failureCount++ only (EP-012D D-004 [P0]) | US-012-20 |
| F3 | GSTR-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/Teams | NONE — no schema validator anywhere (EP-012C D-004 [P0]) | US-012-04 |
| F4 | Export job OOM on 500K-row report | Pre-flight row-estimate via EXPLAIN ANALYZE; reject with 429 + {code:'REPORT_TOO_LARGE', estimatedRows, max:100000} BEFORE format step | NONE — no row-estimate gate (EP-012D D-008 [P0] same fix) | US-012D-01 + US-012-015 |
| F5 | Scheduled 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 bounces | NONE — no bounce handler wired; SES bounce SNS topic exists per OP-1 module but no analytics-side consumer | US-012-20 [SME-PROPOSED] |
| F6 | Multi-property aggregation race — property B's MV refresh runs mid-query; report sums stale-from-A + fresh-from-B | Atomic 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 UI | NONE — no transaction wrapping, no docs | [SME-PROPOSED] US-012D-NEW-MULTI-PROP |
| F7 | Anomaly detector consumer lagged; MV has new HIGH/CRITICAL row but banner doesn't fire | Banner 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 |
| F8 | Drill-down infinite loop (A → B → A) | Cycle detection in drill-annotate using breadcrumb stack; reject with DRILL_CYCLE_DETECTED 400 | NONE — no cycle guard | [SME-PROPOSED] US-012B-NEW-DRILL-SHEET |
| F9 | Cross-tenant data leak via forgotten WHERE "tenantId" in def.query | ESLint AST rule fails CI build; ReportDefinitionsRegistrar.register boot-time check throws | NONE — no enforcement (EP-012D D-010 [P1]) | US-012D-01 |
| F10 | Hindi (Devanagari) folio printed via PDF formatter — characters render as boxes | Handlebars template font stack includes Noto Sans Devanagari; Puppeteer launched with --font-render-hinting=none; smoke test renders fixture invoice with Hindi guest name | NONE — not verified | US-012B-03 / US-012D-03 |
| F11 | Tenant 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.
| ID | Type | Severity | Description | Surfaced by step | Cross-ref |
|---|---|---|---|---|---|
| BF-012-D-001 | Cross-EPIC numbering | P0 | EP-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, 11 | EP-012B D-010+D-012; EP-012C D-001; EP-012D D-001 |
| BF-012-D-002 | India-fiscal-year-vs-code | P1 | filters.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 2 | NEW |
| BF-012-D-003 | Cross-property aggregation | P1 | Multi-property roll-ups SUM raw numbers across currencies (e.g. USD property + INR property = nonsense). No def.aggregation.currencyTarget opt-in. | Step 3 | NEW |
| BF-012-D-004 | Resolver timeout + OOM | P0 | No 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, 9 | EP-012D D-008 |
| BF-012-D-005 | Cross-tenant leak risk | P1 | No central enforcer guarantees every def.query includes WHERE "tenantId"=$1. ESLint AST check absent. 63 definitions × 1 author-forgets = full-tenant leak. | Step 5 | EP-012D D-010 |
| BF-012-D-006 | Scheduler retry missing | P0 | EPIC AC-2 "retry once after 15 min, then admin alert" unmet. BullMQ attempts: 1; transient blip → silent miss of monthly P&L delivery. | Step 10 | EP-012D D-004 |
| BF-012-D-007 | Cron validation missing | P1 | ScheduleSavedViewDto.cron accepted without syntax check. Invalid cron persists; silent first-fire failure. | Step 10 | EP-012D D-007 |
| BF-012-D-008 | Stand-alone scheduled-reports UI missing | P0 | Per-saved-view dialog only; no /reports/scheduled page. Operators can't answer "did my Monday report fire?" without SSH+SQL. | Step 10 | EP-012D D-005 |
| BF-012-D-009 | NIC schema v3.4 validation absent | P0 | "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 12 | EP-012C D-004 |
| BF-012-D-010 | GSTR-3B ITC always 0 | P0 | Net tax payable understates by full ITC amount; filing verbatim overpays. No itcStubbed flag, no manual override entity. | Step 12 | EP-012C D-006 |
| BF-012-D-011 | Hash-chain audit log absent | P0 | Catalog + spec claim AuditLog { entryHash, previousHash } + GET /auditlog/verify. None of it exists. CERT-In + RBI audit posture is theatrical, not real. | Step 11 | EP-012C D-008 |
| BF-012-D-012 | CDNR not exported to GSTR-1 Section 9 | P0 | CreditNote.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 12 | EP-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
| Step | Spans | Why cross-EPIC |
|---|---|---|
| 1 → 4 | EP-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 → 6 | EP-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. |
| 7 | EP-012B (drill targets) ↔ EP-012D (drill plumbing) | Drill cells annotated by D's drill-annotate step; targets are B's def.drillDown declarations. |
| 8 → 9 | EP-012D (formatters + export queue) ↔ EP-012B (PDF letterhead spec) | Formatters in D; per-report PDF templates in B's US-012B-03 (Hotel Letterhead). |
| 10 | EP-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. |
| 11 | EP-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. |
| 12 | EP-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:
- 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.
- 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.
- Scheduled delivery spans 3 services (Step 10) — analytics → notification → SES; failure in any one is opaque to the operator today.
- Audit log is two non-overlapping things (Step 11) —
ReportRunis real; hash-chainAuditLogis fiction. Catalog must distinguish. - GSTR-1 has two implementations (Step 12) — retire frontdesk path; route gateway → analytics
gst-reportv2; 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]) —
ReportRoleOverridemodel to granthousekeeping-productivityto a customOPS_LEADrole without redeploying. - Saved-view role-scoped sharing (US-012D-06) — replace
shared: booleanwithvisibility: '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
BF-001-tenant-property-onboarding.md— template (pilot, 2026-05-24)../reconciliation/PLAYBOOK.md— sequence + drift criteria../reconciliation/EP-012B-rtm.md— Financial Reports per-EPIC RTM (12 drift findings)../reconciliation/EP-012C-rtm.md— Tax & Compliance per-EPIC RTM (11 drift findings)../reconciliation/EP-012D-rtm.md— Report Builder & Scheduled Delivery per-EPIC RTM (11 drift findings)../reconciliation/RTM-DASHBOARD.md— platform-wide roll-uphmp-infrastructure/testing/flow-tests/src/flows/reports-export.flow.spec.ts— Step 8 tripwire (currently soft-skip)hmp-infrastructure/testing/flow-tests/src/flows/saved-views-schedules.flow.spec.ts— Step 10 end-to-end (8 scenarios; not in 13-flow doc per EP-012D D-002)hmp-infrastructure/testing/flow-tests/src/flows/gst-tax-flags.flow.spec.ts— Step 12 GSTR-1/3B + TDS coverage (P0-5/P0-6/P0-7/P0-11)