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-repoCLAUDE.mdGotchas tables (hmp-analytics-service, hmp-web-dashboard) per the audit ground-truth rule. No flow-test specs target EP-012E directly today.
Status legend
| Status | Meaning |
|---|---|
Built | Code exists + tests exist + tests pass |
Built-Untested | Code exists, no automated test coverage (or test exists but is a stub) |
Partial | Code exists for some acceptance criteria, gaps for others |
Stub | Code is a placeholder / TODO / returns mock data |
Not Started | No code |
Summary
| Metric | Count |
|---|---|
| Total stories | 9 |
| EPIC body claims "Not Started" | 8 |
| EPIC body claims "Partial" | 1 (US-012E-05) |
| Verified-Built | 0 |
| Built-Untested | 0 |
| Partial | 3 (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) |
| Stub | 3 (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 Started | 3 (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 findings | 9 (D-001..D-009; 4 P0 / 4 P1 / 1 P2) |
| [SME-PROPOSED] refinements | 14 |
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/actionshmp-analytics-service/src/dashboard/dashboard.service.ts—getTodayPulse,getRevenuePulse,getLaborPulse,getExceptions,getActionshmp-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/*) andActionsQueueSectionperhmp-web-dashboard/CLAUDE.md - No dedicated
/control-centerpage anymore — Operations-Hub Phase 1+2 launched 2026-05-02 and deletedsrc/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/actionsall 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 onnight.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/exceptionsSHALL 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 awarnings[]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_definitionstable (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 viaPropertyServiceClient.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 checkoutSHALL 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/laborhmp-analytics-service/src/dashboard/dashboard.service.ts—getLaborPulse()(consumesMvHousekeepingKpi+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/laboris 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, theMvHkStaffProductivityMV exists (Plan 2.1) and is populated byHousekeepingConsumerwhenassignedTois 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
ShiftPrisma 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 fromUser.lastLoginAt+ activity logs — the latter is fragile and won't capture HK staff who log in once per day. cost per occupied roomrequires 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) orUS-002-NEW-SHIFTS(EP-002) "Staff shifts, rosters, and clock-in/out" before US-012E-03 is testable. Acceptance:Shiftmodel 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 asWARMING_UPin 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/recalculatehmp-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 UTChmp-analytics-service/src/reporting/anomaly/root-cause-hint-engine.ts— 5 rule-based hints attached to HIGH/CRITICALhmp-web-dashboard/src/app/(dashboard)/anomalies/page.tsx— full sortable + dismissable UIhmp-web-dashboard/src/lib/api/anomalies.ts— typed client
Unit + integration tests:
hmp-analytics-service/src/reporting/anomaly/anomaly.controller.spec.tshmp-analytics-service/src/reporting/anomaly/anomaly.service.spec.tshmp-analytics-service/src/reporting/anomaly/anomaly-cron.service.spec.tshmp-analytics-service/src/reporting/anomaly/anomaly-detector.service.spec.tshmp-analytics-service/src/reporting/anomaly/anomaly-notification-handler.service.spec.tshmp-analytics-service/src/reporting/anomaly/detection-strategies.spec.tshmp-analytics-service/src/reporting/anomaly/root-cause-hint-engine.spec.tshmp-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
/anomaliessurface is KPI-statistical (z-score deviation in occupancy/adr/revenue/revpar). These are different exception classes — overlap is partial. - A
MvDailyDeparturesrow + aMvDailyOccupancyrow 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 (
/anomaliesis 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=yesterdayAND/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 carrieskind: OPERATIONALso 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-statshmp-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 — populatesMvHkStaffProductivitywhenassignedTois 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 inMvHkStaffProductivity(which hasavgCleanTimeMinper 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 byPdfFormatterdoes 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
MvRoomTurnaroundMV (or an extension ofMvHkStaffProductivitywith aturnaroundFromCheckoutMincolumn) SHALL be populated by a join consumer that listens on BOTHreservation.checked_out(setscheckoutAt) ANDhousekeeping.task.completed(setscleanedAt), 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
MvRoomTurnaroundand joining to property-service'sRoom.floorfield viaPropertyServiceClient.listRoomsByProperty(propertyId). Cache 5-min via the report-execution-servicecacheTtlSecfield."
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-productionreport (Plan 2.3, MV-backed againstMvChannelPerformance,comparative.lysupported)hmp-analytics-service/prisma/schema.prisma—MvChannelPerformance(property + date + channel: bookingCount, revenue, commission, avgLeadTime)hmp-analytics-service/src/consumers/—ChannelConsumerpopulates the MVhmp-web-dashboard/src/lib/hooks/use-revenue-manager.ts—useRevenueChannelMixcallsGET /channel-stats/summary(channel-service, not analytics)- Dashboard
/distribution/healthpage — 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-productionreport). The missing piece is the curated/analytics/channelsAPI 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) supportslyalready via theLyEnricher—channel-production-reportcould pick that up but the EPIC'suseRevenueChannelMixhook 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_DATAto 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-reportwithcompare: { 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/cellswrites. 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 inMvFrontOfficePace(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: trueflag 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.totalRoomsover the trailing 90 days for the same room type. When <30 days of data exist, the response SHALL includereason: '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
OwnerPortalModuleinhmp-analytics-service/src/ - No
OWNERrole string inDASHBOARD_ROLESindashboard.controller.ts(verified — onlySUPER_ADMIN, ADMIN, OWNER, MANAGER, FRONT_DESK, RESERVATIONS, HOUSEKEEPING, ACCOUNTS, STAFF)- Correction during SME pass:
OWNERIS present in the analytics dashboard role array (dashboard.controller.ts:27-37). The role string exists in analytics-service. Whether it is enforceable inauth-service's RBAC enum requires separate verification (not done in this scaffold pass — Task 6 verification gap).
- Correction during SME pass:
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-serviceexpense.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 byLocaleResolverService+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 newProperty.monthlyDebtServicefield (auth-service or property-service) populated by the GM at property setup; until set, NOI returnsnullwithreason: '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 atauth-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
PilotHealthModuleinhmp-analytics-service/src/ MetricsModulefrom@hmp/observabilityis "Built" per the EPIC dependency table — buthmp_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"Emithmp_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.thresholdstable updatable without redeploy."