Skip to main content

EP-012E — Operational Analytics RTM

EPIC: EP-012E Operational Analytics Parent EPIC: EP-012 Analytics, Reporting & Night Audit Reconciled: 2026-05-25 Pattern: per-EPIC RTM scaffold + SME deep-dive layer per ./PLAYBOOK.md. Catalog note: EP-012E is one of five sub-epics (EP-012A/B/C/D/E) split from the parent EP-012 catalog row (which lists 18 P0/P1 stories). The parent catalog does NOT enumerate EP-012E's 9 stories individually — this RTM is the first per-sub-epic reconciliation and surfaces that as drift D-001. Source of truth for "% passing": per-repo CLAUDE.md Gotchas tables (hmp-analytics-service, hmp-web-dashboard) per the audit ground-truth rule. No flow-test specs target EP-012E directly today.

Status legend​

StatusMeaning
BuiltCode exists + tests exist + tests pass
Built-UntestedCode exists, no automated test coverage (or test exists but is a stub)
PartialCode exists for some acceptance criteria, gaps for others
StubCode is a placeholder / TODO / returns mock data
Not StartedNo code

Summary​

MetricCount
Total stories9
EPIC body claims "Not Started"8
EPIC body claims "Partial"1 (US-012E-05)
Verified-Built0
Built-Untested0
Partial3 (US-012E-01 — control-center scaffolded with stubs; US-012E-04 — anomaly-driven exceptions surface shipped via Plan 3.1; US-012E-05 — HK KPI dashboard + housekeeping-productivity report definition shipped)
Stub3 (US-012E-03 — /control-center/labor stub per CLAUDE.md; US-012E-08 — owner-portal route shell + sidebar tile shipped but no /analytics/owner-portal endpoint or OWNER RBAC role; US-012E-09 — pilot route shell shipped but no /analytics/pilot-health/:tenantId endpoint)
Not Started3 (US-012E-02 SOP; US-012E-06 channel analytics — gap because RM useRevenueChannelMix calls /channel-stats/summary on channel-service, not the EP-012E /analytics/channels endpoint; US-012E-07 overbooking risk)
Drift findings9 (D-001..D-009; 4 P0 / 4 P1 / 1 P2)
[SME-PROPOSED] refinements14

US-012E-01 — Control Center (Operational Status Panel)​

Catalog / EPIC-body status: Not Started (P2, 8 points) Verified status: Partial — /control-center/* controller + dashboard tiles scaffolded; 3 of 5 endpoints (labor, exceptions, actions) return stubs per hmp-analytics-service/CLAUDE.md; tile aggregation across departments (HK + frontdesk + POS) NOT wired end-to-end.

Code locations:

  • hmp-analytics-service/src/dashboard/dashboard.controller.ts:270-356 — GET /control-center/today, /control-center/revenue, /control-center/labor, /control-center/exceptions, /control-center/actions, alias /actions
  • hmp-analytics-service/src/dashboard/dashboard.service.ts — getTodayPulse, getRevenuePulse, getLaborPulse, getExceptions, getActions
  • hmp-analytics-service/src/dashboard/dashboard.service.labor.spec.ts — labor tile real-KPI test (FIND-P2-15)
  • Dashboard tiles via Operations-Hub home (hmp-web-dashboard/src/components/modules/operations-hub/*) and ActionsQueueSection per hmp-web-dashboard/CLAUDE.md
  • No dedicated /control-center page anymore — Operations-Hub Phase 1+2 launched 2026-05-02 and deleted src/app/(dashboard)/control-center/**; /control-center/* URLs 308-redirect to /

Unit + integration tests:

  • hmp-analytics-service/src/dashboard/dashboard.service.labor.spec.ts — labor tile real-data + graceful-degrade specs (FIND-P2-15)
  • hmp-analytics-service/src/dashboard/dashboard.service.metrics-mv.spec.ts — MV-backed metrics
  • Operations-Hub tile tests under hmp-web-dashboard/src/components/modules/operations-hub/__tests__/

Flow tests: none — no flow-test spec targets /control-center/* aggregation across departments.

Real-backend smokes: none specific.

E2E (Playwright): covered transitively by operations-hub home smokes.

Use cases referencing this story: none.

SME notes:

  • The CLAUDE.md ground-truth says /control-center/labor, /exceptions, and /actions all return stubs. The acceptance scenario "All Clear — Hotel operating normally" is therefore unmet today because the aggregation is fake. EPIC says "Not Started" but the controller and tile shell exist — closer to Partial than Not Started.
  • The Operations Hub launch (2026-05-02) deleted the dedicated /control-center/* dashboard pages. The control-center surface is now the operations-hub home tiles. The EPIC body still describes a "single operational control center screen" as if it were its own page — that drifts from the shipped IA.
  • [SME-PROPOSED] Indian-ops cadence: the GM control center is checked at 09:30 (post-night-audit), 14:00 (mid-day pulse for arrivals-not-yet-arrived), 18:00 (evening readiness for dinner + late check-ins), and 22:30 (next-day arrivals readiness). The 60-second Redis TTL acceptance criterion is fine for the 14:00 / 18:00 hits but the 09:30 hit needs to see authoritative post-night-audit numbers; that requires the Kafka invalidation on night.audit.completed, not just TTL.

Drift findings:

D-001 — [P0] [Catalog-gap] EP-012E is NOT in 01-epic-catalog.md — the parent EP-012 row covers only the 18 P0/P1 stories from the un-split EPIC. A new engineer reading the catalog will not know EP-012E exists; they will look for US-012E-01 in EP-012 and find nothing.

  • [SME-PROPOSED] Add EP-012A/B/C/D/E sub-epic rows to 01-epic-catalog.md (under §EP-012 or as siblings) with their 9-story breakdowns. Until that lands, the 5 sub-epic RTMs are stranded.

D-002 — [P1] [Stub-shipped-as-real] getLaborPulse, getExceptions, getActions are stubs per hmp-analytics-service/CLAUDE.md ("Control center labor, exceptions, and actions endpoints return stubs."). The operations-hub ActionsQueueSection queries /actions and the dashboard CLAUDE.md says "the underlying service returns a stub []". US-012E-01 acceptance criteria require real cross-department counts (Arrivals not checked in, Rooms pending, Open complaints, Maintenance > 24h, POS KOT queue) — today the home tile renders zero values that look like real "all clear" output, masking the gap.

  • [SME-PROPOSED] Add AC: "GET /control-center/exceptions SHALL aggregate from reservation-service (arrivals past ETA), housekeeping-service (rooms pending >1h before arrival), frontdesk-service (open service-recovery folio notes), housekeeping-service (maintenance >24h), and pos-service (KOT queue depth). Each upstream SHALL contribute a warnings[] entry on failure; the tile SHALL render an amber 'partial data' banner when warnings.length > 0 instead of silently showing zeros."

US-012E-02 — SOP Management & Compliance Tracking​

Catalog / EPIC-body status: Not Started (P2, 13 points) Verified status: Not Started

Code locations: NONE FOUND. No sop_definitions Prisma model in hmp-property-service (no model match for sop|standardOperatingProcedure); no /analytics/sop controller in hmp-analytics-service; no SOP dashboard page (no app/(dashboard)/settings/sop or app/(dashboard)/sop route).

Unit + integration tests: NONE FOUND.

Flow tests: NONE FOUND.

Use cases referencing this story: none.

SME notes:

  • "Not Started" is honest — zero code surface.
  • The EPIC implementation note "SOP definitions stored in a new sop_definitions table (property-service); compliance events sourced from housekeeping task timestamps and complaint resolution logs" puts the data model in property-service but the read API in analytics-service. That's a 2-service handoff with no defined contract today.
  • Indian-hospitality reality: a property manager's "SOP compliance" report is usually one of (a) "rooms cleaned within X minutes of checkout", (b) "guest complaints resolved within Y hours", (c) "F&B order delivered within Z minutes of KOT". All three already have raw event sources in HK / frontdesk / POS — the SOP layer is a definition + threshold + aggregation, not new data collection.
  • [SME-PROPOSED] SOP definitions are tenant-customisable AND property-customisable — global chain SOPs override-able per property. Today the EPIC body doesn't mention scope.

Drift findings:

D-003 — [P1] [Story-incomplete + cross-service-boundary] SOP definitions live in property-service per the EPIC's §10.6 implementation note; compliance reads in analytics-service. No story defines the contract (what events does property-service emit when an SOP fires? Does analytics subscribe to a new sop.compliance.* topic, or does it scan existing housekeeping.task.completed + frontdesk.complaint.resolved + pos.kot.completed events?).

  • [SME-PROPOSED] Add AC: "SOP definitions SHALL be CRUD'd via POST/PUT/GET/DELETE /api/properties/:id/sop-definitions (property-service) with fields (name, eventType, threshold, thresholdUnit, scope: PROPERTY|CHAIN). Analytics-service SHALL read those definitions via PropertyServiceClient.listSopDefinitions(tenantId) and aggregate compliance % from the existing event topics (HK + frontdesk + POS) — no new emit needed."
  • [SME-PROPOSED] Add AC: "SOP compliance SHALL be scoped — a chain-level SOP Room cleaned within 3h of checkout SHALL be inheritable per property (property can override the 3h threshold or opt out)."

US-012E-03 — Labor Analytics​

Catalog / EPIC-body status: Not Started (P2, 8 points) Verified status: Stub — /control-center/labor exists and has a real-data spec (dashboard.service.labor.spec.ts); no dedicated /analytics/labor per-shift report; HR shift-data integration with HK is not wired.

Code locations:

  • hmp-analytics-service/src/dashboard/dashboard.controller.ts:296 — GET /control-center/labor
  • hmp-analytics-service/src/dashboard/dashboard.service.ts — getLaborPulse() (consumes MvHousekeepingKpi + MvPosRevenue)
  • hmp-analytics-service/src/dashboard/dashboard.service.labor.spec.ts — FIND-P2-15 real-KPI tests + graceful-degrade

Unit + integration tests:

  • hmp-analytics-service/src/dashboard/dashboard.service.labor.spec.ts — present

Flow tests: none. Real-backend smokes: none. Use cases referencing this story: none.

SME notes:

  • The shipped /control-center/labor is a pulse tile (today's snapshot), not the per-shift / per-attendant / cost-per-occupied-room analytics the EPIC describes. The EPIC AC requires "shifts logged, rooms cleaned per attendant, check-ins per front desk agent, cost per occupied room" — none of those four are computed today.
  • Per hmp-analytics-service/CLAUDE.md, the MvHkStaffProductivity MV exists (Plan 2.1) and is populated by HousekeepingConsumer when assignedTo is present. That's the right source for "rooms cleaned per attendant" — the story should reference it explicitly.
  • Shift data is not modeled anywhere in HMP today. No Shift Prisma model exists in auth-service or property-service. "Shifts logged" requires either (a) a new shift entity (rosters + clock-in/out), or (b) deriving shift windows from User.lastLoginAt + activity logs — the latter is fragile and won't capture HK staff who log in once per day.
  • cost per occupied room requires payroll data — out of scope for HMP and explicitly called out in the EPIC §5 dependency table as "requires shift data integration".

Drift findings:

D-004 — [P0] [Story-incomplete + missing-upstream-data] Story assumes a shift-data source that does not exist in HMP. Without a Shift entity (rosters, clock-in/clock-out, hourly rate), "cost per occupied room" cannot be computed; "rooms cleaned per attendant" can use MvHkStaffProductivity but lacks the hours-worked denominator that productivity requires.

  • [SME-PROPOSED] Author a new story US-001-NEW-SHIFTS (EP-001) or US-002-NEW-SHIFTS (EP-002) "Staff shifts, rosters, and clock-in/out" before US-012E-03 is testable. Acceptance: Shift model with (userId, propertyId, scheduledStart, scheduledEnd, actualStart, actualEnd, hourlyRate?); clock-in/out endpoints; rota CRUD.
  • [SME-PROPOSED] Add AC: "Rooms-per-attendant SHALL be sourced from MvHkStaffProductivity (Plan 2.1 — keyed by (tenant, property, date, staffId)). When the source MV has 0 rows for a property/date, the labor tile SHALL render 'No HK assignments recorded' rather than a misleading 0 — same pattern as WARMING_UP in the reporting comparator."

US-012E-04 — Daily Exceptions Summary​

Catalog / EPIC-body status: Not Started (P2, 5 points) Verified status: Partial — /control-center/exceptions controller endpoint exists but returns a stub (D-002); the analogous "anomaly-driven" surface IS shipped via Plan 3.1 /anomalies + 4 detection strategies + cron + MvAnomaly table, which arguably satisfies the spirit of "daily exceptions" for KPI-level deviations.

Code locations:

  • hmp-analytics-service/src/dashboard/dashboard.controller.ts:309 — GET /control-center/exceptions (stub)
  • hmp-analytics-service/src/reporting/anomaly/anomaly.controller.ts — 4 endpoints: GET /anomalies, GET /anomalies/:id, PATCH /anomalies/:id/dismiss, POST /anomalies/recalculate
  • hmp-analytics-service/src/reporting/anomaly/anomaly-detector.service.ts — 3 strategies (STATISTICAL/SEASONAL/FORECAST_RESIDUAL) for 4 metrics (occupancy/adr/revenue/revpar)
  • hmp-analytics-service/src/reporting/anomaly/anomaly-cron.service.ts — @Cron('30 3 * * *') daily UTC
  • hmp-analytics-service/src/reporting/anomaly/root-cause-hint-engine.ts — 5 rule-based hints attached to HIGH/CRITICAL
  • hmp-web-dashboard/src/app/(dashboard)/anomalies/page.tsx — full sortable + dismissable UI
  • hmp-web-dashboard/src/lib/api/anomalies.ts — typed client

Unit + integration tests:

  • hmp-analytics-service/src/reporting/anomaly/anomaly.controller.spec.ts
  • hmp-analytics-service/src/reporting/anomaly/anomaly.service.spec.ts
  • hmp-analytics-service/src/reporting/anomaly/anomaly-cron.service.spec.ts
  • hmp-analytics-service/src/reporting/anomaly/anomaly-detector.service.spec.ts
  • hmp-analytics-service/src/reporting/anomaly/anomaly-notification-handler.service.spec.ts
  • hmp-analytics-service/src/reporting/anomaly/detection-strategies.spec.ts
  • hmp-analytics-service/src/reporting/anomaly/root-cause-hint-engine.spec.ts
  • hmp-web-dashboard/src/app/(dashboard)/anomalies/anomalies-page.test.tsx

Flow tests: none. Real-backend smokes: none specific to /anomalies.

Use cases referencing this story: none.

SME notes:

  • The EPIC body's "Daily Exceptions Summary" frames exceptions as operational (folio discrepancies, unresolved complaints, no-shows, maintenance overdue). The shipped /anomalies surface is KPI-statistical (z-score deviation in occupancy/adr/revenue/revpar). These are different exception classes — overlap is partial.
  • A MvDailyDepartures row + a MvDailyOccupancy row for the same date already enable a no-show count and a folio-discrepancy heuristic; the EPIC's "exceptions" tile should aggregate those alongside the statistical anomalies, not duplicate them.
  • Daily ops meeting cadence (07:30 morning huddle in most Indian properties): the GM wants ONE screen for "what broke yesterday" — anomalies + operational exceptions on the same page. Today they're split (/anomalies is its own page; control-center exceptions is a stub).

Drift findings:

D-005 — [P1] [Two-surface-overlap] Statistical anomalies (/anomalies) and operational exceptions (/control-center/exceptions) are conceptually the same "what broke yesterday" surface for the GM but live in two places — one shipped + tested, one stub. The GM has to know which screen has which type.

  • [SME-PROPOSED] Author a unified "Daily Exceptions Digest" surface (mounted on the operations-hub home as ExceptionsRail) that fans out to BOTH /anomalies?dismissed=false&severity=HIGH,CRITICAL&from=yesterday AND /control-center/exceptions. The dashboard composes the two; backends stay separate (anomaly detection has its own cron + ML; operational exceptions are a live aggregator).
  • [SME-PROPOSED] Add AC: "Operational exceptions SHALL include no-show count (MvDailyOccupancy.noShows), folio-balance > 0 at checkout (frontdesk-service /folios?balance>0&checkoutDate=yesterday), maintenance > 24h (asset-service /work-orders?status=OPEN&openedAt<24h), and complaints unresolved >12h (frontdesk-service). Each row carries kind: OPERATIONAL so the dashboard can deduplicate against anomaly rows on the same (propertyId, date, metric) key."

US-012E-05 — Housekeeping KPIs & Turnaround Analytics​

Catalog / EPIC-body status: Partial (P2, 8 points) Verified status: Partial — GET /analytics/dashboard/housekeeping-stats shipped + housekeeping-productivity report definition shipped (Plan 2.3) + MvHkStaffProductivity MV shipped (Plan 2.1) + dashboard /housekeeping/kpis page shipped. Per-attendant + per-floor turnaround time analytics: not fully wired through to dashboard.

Code locations:

  • hmp-analytics-service/src/dashboard/dashboard.controller.ts:166 — GET /analytics/dashboard/housekeeping-stats
  • hmp-analytics-service/src/dashboard/dashboard.service.ts — getHousekeepingStats()
  • hmp-analytics-service/src/reporting/reports/housekeeping-productivity.definition.ts + spec (Plan 2.3)
  • hmp-analytics-service/src/consumers/ — HousekeepingConsumer (extended Plan 2.1 — populates MvHkStaffProductivity when assignedTo is present)
  • hmp-analytics-service/prisma/schema.prisma — MvHkStaffProductivity (tenant + property + date + staffId)
  • hmp-web-dashboard/src/app/(dashboard)/housekeeping/kpis/page.tsx — KPI page

Unit + integration tests:

  • hmp-analytics-service/src/reporting/reports/housekeeping-productivity.definition.spec.ts

Flow tests: none specific to HK KPIs. Real-backend smokes: none specific.

Use cases referencing this story: none.

SME notes:

  • The MV is correctly keyed by staffId — the per-attendant table from the EPIC's acceptance scenario IS computable today.
  • room-turnaround-time (time from checkout to room marked CLEAN) is not enumerated in MvHkStaffProductivity (which has avgCleanTimeMin per staff). Turnaround = checkout-to-CLEAN spans the frontdesk handoff (room status set to DIRTY) + HK cleaning. Today's MV captures HK cleaning only.
  • The EPIC's "Floor 4 view" requires joining HK tasks → rooms → floor. The MV doesn't carry floor; the report definition would have to join through the property-service room table — that's a cross-service read at query time. Honestly, this AC is not testable from the analytics-service alone.
  • [SME-PROPOSED] Hindi-speaking attendants in the AC sample (Sunita Devi, Rekha Singh) — names are good ground-truth for Indian-hospitality scenarios but the story should explicitly require Unicode name rendering in the PDF/Excel exports (Hindi script support in the Puppeteer-generated PDF — the EPIC body doesn't mention this and the Chromium base used by PdfFormatter does NOT include Devanagari fonts by default in Alpine).

Drift findings:

D-006 — [P1] [Story-incomplete + cross-service-data] Room turnaround time (checkout → CLEAN) requires correlating frontdesk's checkout timestamp with housekeeping's CLEAN timestamp — the analytics MV MvHkStaffProductivity captures only the HK side. Today there is no single MV that captures the full turnaround.

  • [SME-PROPOSED] Add AC: "A new MvRoomTurnaround MV (or an extension of MvHkStaffProductivity with a turnaroundFromCheckoutMin column) SHALL be populated by a join consumer that listens on BOTH reservation.checked_out (sets checkoutAt) AND housekeeping.task.completed (sets cleanedAt), computing the delta and writing the row when both events are seen for the same room+date."
  • [SME-PROPOSED] Add AC: "Per-floor turnaround SHALL be computed by querying MvRoomTurnaround and joining to property-service's Room.floor field via PropertyServiceClient.listRoomsByProperty(propertyId). Cache 5-min via the report-execution-service cacheTtlSec field."

US-012E-06 — Channel Analytics & Booking Pace​

Catalog / EPIC-body status: Not Started (P2, 8 points) Verified status: Not Started for the EP-012E surface — MvChannelPerformance exists and the channel-production report definition (Plan 2.3) is shipped, but no /analytics/channels?from=&to=&propertyId= endpoint exists. The dashboard's useRevenueChannelMix hook (RM page) calls /channel-stats/summary on channel-service, NOT analytics-service.

Code locations:

  • hmp-analytics-service/src/reporting/reports/channel-production.definition.ts — channel-production report (Plan 2.3, MV-backed against MvChannelPerformance, comparative.ly supported)
  • hmp-analytics-service/prisma/schema.prisma — MvChannelPerformance (property + date + channel: bookingCount, revenue, commission, avgLeadTime)
  • hmp-analytics-service/src/consumers/ — ChannelConsumer populates the MV
  • hmp-web-dashboard/src/lib/hooks/use-revenue-manager.ts — useRevenueChannelMix calls GET /channel-stats/summary (channel-service, not analytics)
  • Dashboard /distribution/health page — connectivity dashboard, separate from booking-pace

Unit + integration tests:

  • hmp-analytics-service/src/reporting/reports/channel-production.definition.spec.ts

Flow tests: none. Real-backend smokes: none.

Use cases referencing this story: none.

SME notes:

  • The data IS available (MvChannelPerformance + channel-production report). The missing piece is the curated /analytics/channels API the EPIC body specifies, with pace + STLY (Same Time Last Year) + 30-day mix.
  • "Same Time Last Year" (STLY) is the load-bearing comparison in Indian hospitality — wedding-season + Diwali ADRs only make sense vs the prior year. The ReportComparator (Plan 5) supports ly already via the LyEnricher — channel-production-report could pick that up but the EPIC's useRevenueChannelMix hook doesn't consume the v2 report runner.
  • "Pickup Gap" copy in the acceptance criterion ("MakeMyTrip is 18% behind last year's pace for this period") requires an LY enricher reason of PARTIAL_LY_DATA to be surfaced when MMT's prior-year row is incomplete — otherwise the gap reads as 100% deficit, which would alarm a Revenue Manager every Diwali.

Drift findings:

D-007 — [P1] [Story-vs-implementation + two-paths] EP-012E expects /analytics/channels on analytics-service; the RM page already gets channel mix from channel-service's /channel-stats/summary. Today there are two channel-analytics paths with overlapping concerns — analytics for pace + LY, channel-service for live mix. EPIC doesn't acknowledge the split.

  • [SME-PROPOSED] Refine US-012E-06 boundary: "Channel-service owns live channel-mix (rolling 30 days, percentage of revenue by source) — used by the RM dashboard. Analytics-service owns booking-pace + STLY + Pickup Gap — backed by channel-production-report with compare: { ly: true }. The two surfaces SHALL link to each other (channel-mix tile carries a 'See pacing' link to /reports/v2/channel-production-report?compare.ly=true)."
  • [SME-PROPOSED] Add AC: "When LY data for a channel is missing (e.g. property was not yet on the OTA last year), the response SHALL include ly: { value: null, reason: 'NO_LY_DATA' } and the dashboard SHALL render '— no LY baseline' instead of '–100% pace'. Test asserts the reason code is present."

US-012E-07 — Overbooking Risk & Inventory Advisory​

Catalog / EPIC-body status: Not Started (P2, 8 points) Verified status: Not Started

Code locations: NONE FOUND. No /analytics/overbooking endpoint on analytics-service. No OverbookingRiskService. No MvOverbooking MV. The dashboard does have an useOverbookingRisk hook? Quick scan: no, it does not.

The historical no-show rate data IS in MvDailyOccupancy.noShows (property + date). The forward-looking overbooking confirmed-count is computable from reservation-service's /reservations?arrivalDate=<date>&status=CONFIRMED. The binomial walk-probability formula is plain math. None of this is wired today.

Unit + integration tests: NONE FOUND. Flow tests: NONE FOUND. Real-backend smokes: NONE FOUND.

Use cases referencing this story: none.

SME notes:

  • The EPIC body's formula P(walk) = 1 − Binomial.CDF(rooms_available, confirmed_count, 1 − historical_noshow_rate) is correct mathematically. Implementation should be a utility function (no need for a microservice or sidecar).
  • The "Close inventory on all OTA channels" one-click action is the dangerous bit: it cascades into channel-service /distribution/ari/cells writes. The story should require a confirm dialog with the inventory impact preview before the click commits.
  • Real-world: most Indian Tier-2/3 hotels with <50 rooms don't deliberately overbook; this story is more relevant to chains. P2/P3 for Property #1.
  • [SME-PROPOSED] Walk-probability bands need to factor in MvFrontOfficePace (Plan 2.1) — booking pace this week vs same week LY is a stronger signal than raw confirmed-count for the rare Property-1-scale operator.

Drift findings: (none net new — captured in summary as Not Started)

  • [SME-PROPOSED] Add AC: "Overbooking advisory SHALL require a confirmInventoryClose: true flag in the POST body before triggering channel-close. The endpoint SHALL return both the projected walk-probability AND the inventory-close preview (rooms that would be released by channel × room type) so the operator confirms before commit."
  • [SME-PROPOSED] Add AC: "Historical no-show rate SHALL be computed from MvDailyOccupancy.noShows / MvDailyOccupancy.totalRooms over the trailing 90 days for the same room type. When <30 days of data exist, the response SHALL include reason: 'WARMING_UP' and the UI SHALL show 'Insufficient history — no advisory' rather than a default 0% risk."

US-012E-08 — Owner / Investor Portal​

Catalog / EPIC-body status: Not Started (P2, 13 points) Verified status: Stub — dashboard route shell shipped (/owner-portal/page.tsx + sidebar tile per hmp-web-dashboard/CLAUDE.md); no /analytics/owner-portal endpoint on analytics-service; no OWNER role in auth-service's role enum; no NOI / GOP / capex computation pipeline.

Code locations:

  • hmp-web-dashboard/src/app/(dashboard)/owner-portal/page.tsx — shell only (re-exports ./keynova)
  • hmp-web-dashboard/src/app/(dashboard)/owner-portal/keynova.tsx — UI shell (not verified in this pass; likely contains placeholder copy)
  • No corresponding OwnerPortalModule in hmp-analytics-service/src/
  • No OWNER role string in DASHBOARD_ROLES in dashboard.controller.ts (verified — only SUPER_ADMIN, ADMIN, OWNER, MANAGER, FRONT_DESK, RESERVATIONS, HOUSEKEEPING, ACCOUNTS, STAFF)
    • Correction during SME pass: OWNER IS present in the analytics dashboard role array (dashboard.controller.ts:27-37). The role string exists in analytics-service. Whether it is enforceable in auth-service's RBAC enum requires separate verification (not done in this scaffold pass — Task 6 verification gap).

Unit + integration tests: NONE FOUND. Flow tests: NONE FOUND. Real-backend smokes: NONE FOUND.

Use cases referencing this story: none.

SME notes:

  • The OWNER role string is referenced by analytics-service but the cascade — auth-service role enum, JWT claim, RBAC guard, signup flow, role assignment UI in /settings/users — is unverified. This is a [CITED-BUT-NOT-FOUND] situation: dashboard says "Owner Portal" sidebar tile, analytics says OWNER role accepted; neither proves auth-service ships the role.
  • NOI = Revenue − Operating Expenses − Debt Service. Revenue is available (folio + POS + banquet + spa). Operating expenses live in MvDailyExpense (Plan 2 — populated from property-service expense.recorded). Debt service is NOT in HMP today — it's a manual owner-supplied field per property.
  • "Capital expenditure log (manual input by GM or Finance Manager)" requires a new CapEx CRUD surface — out of scope for an analytics page. The story conflates owner reporting with new data-entry workflows.
  • [SME-PROPOSED] Indian owners typically expect monthly P&L in the FY-Apr-to-Mar fiscal calendar (already supported by LocaleResolverService + fiscalYearStart: { month: 4, day: 1 } per analytics CLAUDE.md). The portal MUST default to Indian FY, not calendar year.

Drift findings:

D-008 — [P0] [Stub-shipped-as-route + cross-EPIC-RBAC-gap] Owner Portal route + sidebar tile exist on the dashboard but no backend endpoint, no owner-specific RBAC enforcement test, and (likely) no OWNER role provisioning flow in auth-service. Property Admins clicking the sidebar tile see... whatever the keynova.tsx shell renders, with no data. Owners cannot sign in as OWNER today.

  • [SME-PROPOSED] Author a new story US-001-NEW-OWNER-ROLE (EP-001) "Provision OWNER role end-to-end" — auth-service enum extension, signup-by-invitation flow restricted to /owner-portal/* routes, no access to /guests/* or /front-desk/*, JWT claim plumbing. Block US-012E-08 on it.
  • [SME-PROPOSED] Add AC: "GET /analytics/owner-portal?month=&propertyId= SHALL return {occupancy, revpar, totalRevenue, totalExpenses, gop, gopPct, noi, monthOverMonthTrend[]}. NOI computation requires a new Property.monthlyDebtService field (auth-service or property-service) populated by the GM at property setup; until set, NOI returns null with reason: 'NO_DEBT_SERVICE_SET'."
  • [SME-PROPOSED] Add AC: "OWNER role SHALL receive 403 on /guest, /reservations, /housekeeping, /pos, /banquet, /spa, /settings/users, /settings/audit-trail. Test asserts the deny list via a tenant-isolation spec at auth-service/src/rbac/owner-role-deny.spec.ts."

US-012E-09 — Pilot Health Monitoring​

Catalog / EPIC-body status: Not Started (P3, 8 points) Verified status: Stub — dashboard route shell shipped (/pilot/page.tsx); no /analytics/pilot-health/:tenantId endpoint; no hmp_feature_usage_total Prometheus counter; no CS-team Grafana dashboard wiring documented.

Code locations:

  • hmp-web-dashboard/src/app/(dashboard)/pilot/page.tsx — shell only (re-exports ./keynova)
  • hmp-web-dashboard/src/app/(dashboard)/pilot/keynova.tsx — UI shell, not yet verified
  • No PilotHealthModule in hmp-analytics-service/src/
  • MetricsModule from @hmp/observability is "Built" per the EPIC dependency table — but hmp_feature_usage_total{tenant_id, feature} is NOT in any service today (would require an interceptor in every backend service)

Unit + integration tests: NONE FOUND. Flow tests: NONE FOUND. Real-backend smokes: NONE FOUND.

Use cases referencing this story: none.

SME notes:

  • This is the only P3 in EP-012E and explicitly an HMP-internal CS tool — appropriate to defer past Property #1 launch.
  • The implementation requires (a) per-API-call Prometheus counter instrumentation across all 17 backends; (b) a cross-tenant analytics endpoint visible only to SUPER_ADMIN; (c) Grafana dashboard. Item (a) is the heavy lift — it touches every service.
  • Risk-score thresholds (RED <30% / AMBER 30-60% / GREEN >60%) are not calibrated to anything — should be derived from observed adoption distribution across pilot hotels, not invented.

Drift findings:

D-009 — [P2] [Story-design + cross-service-instrumentation] "Pilot health" requires per-feature usage counters across every backend service — a 17-service instrumentation effort hidden inside an analytics story. The EPIC §10 mentions hmp_feature_usage_total Prometheus counter as "Phase 3 deliverable" but doesn't enumerate which service emits it or where the counter list comes from.

  • [SME-PROPOSED] Refine US-012E-09 to be a rollup story dependent on a new infrastructure story INF-NEW-FEATURE-COUNTERS "Emit hmp_feature_usage_total{service, feature, tenant_id} from a shared interceptor in @hmp/observability". The interceptor lives in shared-packages; each service consumes it; no per-service code change beyond a module import.
  • [SME-PROPOSED] Add AC: "Risk-score thresholds SHALL be re-tuned after 30 days of data from the first 10 pilot hotels. Default RED <30% is a placeholder; the real cutoff SHALL be at the p25 of observed adoption across the cohort. Codify as a FeatureAdoptionRiskConfig.thresholds table updatable without redeploy."