Dashboard & Analytics¶
One-line: The presentation and business-intelligence layer of Prism β it turns the raw
orders/customers/campaign_miscollections into the widgets a restaurant sees (Customer Overview, Where Orders Come From, When Do They Buy, Star Ratings, Effective Offers, Loyalty Analysisβ¦), served either through the legacy per-request dashboard or the modern pre-aggregated/statsmetrics API.
Validation legend¶
| Symbol | Meaning |
|---|---|
| β | Verified directly in code |
| π | From PM Excel / business source of truth |
| π» | Code-only (no Excel/business spec) |
| β»οΈ | Dual / duplicated implementation (migration in progress) |
| β°οΈ | Legacy / being deprecated |
| β οΈ | Known bug / risk / caveat |
1. Overview¶
Dashboard & Analytics is the "data out" / presentation plane of the Prism architecture (see High-Level Architecture Β§3). It does not own transactional data β it reads what ingestion, loyalty, campaigns and feedback have already written, and shapes it into KPIs and charts.
The module has two co-existing implementations β»οΈ β this duality is the single most important thing to understand:
| Legacy dashboard β°οΈ | Modern dashboard | |
|---|---|---|
| Controller | controllers/dashboardController.js β
|
controllers/dashboardControllerNew.js β
+ controllers/metricsController.js β
|
| Read strategy | Per-request live queries over dashboard_mis, rating_mis, checkin_mis, addo_campaigns |
Pre-aggregated β a nightly cron (aggregationProcess.js) rolls orders/order_items into ~14 ETL collections; the API only reads those |
| API surface | /crm_api/* endpoints (mixed into routes/crm.js) |
/stats/* metrics endpoints (routes/stats.js) β
+ dashboardControllerNew checklist/graph endpoints |
| Real-time | none | Redis cache + Socket.io β |
| Status | Being replaced π (map-02) | Current target |
Why two? Prism is mid-migration to Next.js (Roadmap Β§A). The legacy monolithic dashboardController (50+ functions) computed everything on the fly, which does not scale. The modern path moves the heavy GROUP BY work to a nightly aggregation cron so read endpoints stay fast β this is the classic materialized-view / MIS pattern.
π The PM Excel "Dashboard & Analytics" sheet defines the widgets and their math. This doc maps each widget to the code path and data source that (should) feed it, and flags the widgets currently affected by known bugs (E1.1, E1.4, E1.9, E1.10).
2. Business Flow¶
2.1 Example journey β a franchise owner opens their dashboard¶
- Owner of a 12-outlet chain logs in; the frontend calls
POST /stats/dashboard/summarywith{ parent_business_id, business_id, start, end }. β authMiddlewarevalidates the staff user + business, setsreq.body.created_by. βmetricsController.getDashboardSummaryfires 10 aggregations in parallel (Promise.all) against the pre-built ETL collections. β- Widgets render: Customer Overview, New vs Returning, Where Orders Come From, Orders Overview, When Do They Buy (hour heatmap), Star Rating Distribution, Digital Bill Insights, Feedback Insights, Loyalty Analysis, Most Selling Item, Your Effective Offers. π
- Owner switches the Filter by Outlet to one outlet β the same endpoints are re-called scoped to a
business_id. - Smart Alert Banners surface anomalies (e.g. "Orders down 30% vs last week", low wallet balance) computed from the summary +
business_configs. π
2.2 Edge cases π (from Business-Flow "Edge Cases & Gotchas")¶
- Channel integration inactive β the widget must still show the channel with
0, never omit it (Dashboard 3.5). βdashboardMISseeds all channel counters withdefault: 0. - Cancelled / refunded orders β excluded from revenue, AOV, most-selling-item; earned points are clawed back.
- Never-ordered customers β counted under Non-Transacting, not dropped.
- Timezone β "When Do They Buy" / hour heatmap use IST day boundaries (
+05:30) in the modern path β (parseDateStrict), and the outlet's local timezone per the Excel. - Period-filter inconsistency β οΈ β widgets do not all share one default period/outlet filter today (Roadmap A4.6, open).
3. Technical Flow¶
3.1 Modern read path (source β API β validation β logic β DB β response)¶
Frontend
β POST /stats/dashboard/summary { parent_business_id, business_id, start, end }
βΌ
authMiddleware β
(validates user+business, sets created_by)
βΌ
metricsController.getDashboardSummary β
β metricsHelper.validateRangeParams(req,res) β date & param validation
βΌ
Promise.all([ β logic (10 parallel aggregations)
aggregateOrdersRange, aggregateFulfillmentRange, aggregateOutletsRange,
aggregateHourHeatmapRange, aggregateMealBreakdownRange, aggregateItemsRange,
aggregateCampaignsRange, aggregateGeoRange, aggregateSpecialDaysRange,
aggregateUnderperformRange ])
βΌ
MongoDB ETL collections β
(orders_by_day_business, fulfillment_breakdown_by_parent, β¦)
βΌ
compute totals: totalOrders, totalRevenue, AOV = totalRevenue/totalOrders, peakHours β
+ business_aggreator lookup for customer block
βΌ
JSON response { orders, fulfillment, outlets, customers, items, campaigns, geo, β¦ }
No events / queues / notifications are emitted by the read path β it is pure read. The write path is the cron (Β§10).
3.2 Modern write path (aggregation cron) β ¶
node-cron "0 0 * * *" (midnight IST) β aggregationController.js
βΌ
insert aggregation_process { status:false } β run tracking
βΌ
find business_configs { is_parent:true, business_status:1, prism_active:1 }
βΌ
aggregationProcess.processAggregationParent(parents) β parent-level rollups
aggregationProcess.processAggregationChild(childIds) β outlet-level rollups
β reads: orders, order_items, customers, customers_outlets, business_configs, promotions
β writes (replaceOne / bulkWrite): 14 ETL collections (see Β§6)
βΌ
update aggregation_process { status:true, completed_at }
βΌ
sendAggregationMail("Aggregation cron completed", β¦) β email notification
3.3 Legacy read path β°οΈ¶
dashboardController endpoints query dashboard_mis, rating_mis, checkin_mis, feedback_configs, google_reviews and addo_campaigns per request and compute widget values inline. dashboardMIS (the daily summary collection) is populated by processLibrary/dashboardMIS.js. This path also owns operational endpoints like whatsappTrack (WhatsApp link-click tracking), businessStatus, and syncCRMToken.
4. Architecture Diagram¶
flowchart TB
subgraph write["Write plane β nightly cron (0 0 * * * IST)"]
AGG[aggregationController.js β
]
AP[processLibrary/aggregationProcess.js β
]
DM[processLibrary/dashboardMIS.js]
CM[processLibrary/campaignMis.js<br/>campaignAggregation.js]
end
subgraph src["Source collections"]
ORD[(orders)]
OI[(order_items)]
CUST[(customers / customers_outlets)]
CFG[(business_configs)]
PROMO[(promotions)]
CMIS[(campaign_mis)]
RMIS[(rating_mis)]
DBILL[(digital_bill_mis)]
end
subgraph etl["ETL / MIS collections (materialized views)"]
E1[(orders_by_day_business)]
E2[(fulfillment_breakdown_by_parent)]
E3[(hour_of_day_heatmap_by_parent)]
E4[(top_selling_items_by_day)]
E5[(β¦ 10 more, see Β§6)]
DMIS[(dashboard_mis)]
BAGG[(business_aggreator)]
end
subgraph read["Read plane β APIs"]
MC[metricsController β
]
MH[commonFunctions/metricsHelper β
]
DCN[dashboardControllerNew β
<br/>checklist + graphs + Redis + Socket.io]
DC[dashboardController β°οΈ legacy]
end
ORD & OI & CUST & CFG & PROMO --> AP --> E1 & E2 & E3 & E4 & E5 & BAGG
ORD --> DM --> DMIS
CMIS --> CM
MC --> MH --> E1 & E2 & E3 & E4 & E5 & BAGG
DC --> DMIS & RMIS & DBILL
Frontend([Frontend]) -->|/stats/*| MC
Frontend -->|checklist/graphs| DCN
Frontend -->|/crm_api/* legacy| DC
DCN -.Redis cache + Socket.io.-> Frontend
5. Folder Structure (real files)¶
controllers/
dashboardControllerNew.js β
modern: checklist, KPIs, graph/canvas gen, Redis, Socket.io (~2560 lines)
dashboardController.js β°οΈ legacy monolith: whatsappTrack, businessStatus, syncCRMToken, past-campaign views (~50 fns)
metricsController.js β
thin controller for /stats/* β delegates to metricsHelper
aggregationController.js β
node-cron trigger for the nightly aggregation
order360Controller.js 360Β° order view
invoiceInsightsController.js digital-bill click/feedback/download tracking
commonFunctions/
metricsHelper.js β
all /stats aggregation + validation logic (ETL COLLECTIONS map, ~486 lines)
processLibrary/
aggregationProcess.js β
ETL engine β 14 materialized collections (~1378 lines)
dashboardMIS.js daily dashboard_mis rollup
campaignAggregation.js campaign send rollups β campaign_aggregation
campaignMis.js campaign ROI/conversion β campaign_mis
routes/
stats.js β
/stats/* (12 endpoints, authMiddleware)
crmv2.js β
/crm_api/v2/dashboarData (businessController) β οΈ no auth middleware
crm.js legacy dashboard endpoints
models/
dashboardMIS.js β
collection: dashboard_mis
dashboard_modules.js β
collection: dashboard_module (widget registry)
campaign_mis.js β
collection: campaign_mis
buisness_aggregator.js β
collection: business_aggreator (customer cohort counts)
6. Database¶
6.1 ETL / materialized-view collections β
(written by aggregationProcess.js, ETL_COLLS map)¶
| Collection | Feeds widget | Purpose |
|---|---|---|
orders_by_day_business |
Orders Overview | Daily orders + revenue per business |
fulfillment_breakdown_by_parent |
Where Orders Come From / order-type split | dinein / delivery / pickup / incar / other |
outlet_metrics_by_day |
Filter by Outlet | Per-outlet daily metrics |
outlet_toplist_by_day |
Outlet leaderboard | Top outlets by revenue/orders |
state_city_distribution_by_day |
Geo widget | State/city order distribution |
hour_of_day_heatmap_by_parent |
When Do They Buy | Orders by hour bucket |
meal_time_breakfast_by_parent β meal_time_breakdown_by_parent |
Meal-time split | Breakfast/lunch/snacks/dinner |
campaign_metrics_by_day |
Campaign block | Campaign sends/metrics by day |
top_selling_items_by_day |
Most Selling Item | Top items by qty/revenue |
item_revenue_contribution_by_day |
Item revenue mix | % revenue per item |
product_pairing_by_day |
Basket analysis | Frequently-bought-together |
category_performance_by_day |
Category widget | Category revenue |
underperforming_items_by_day |
Underperforming items | Bottom-decile items (ETL_UNDERPERF_PCT = 0.10) |
daily_summary_by_parent |
Summary / Smart Alerts | One consolidated daily doc per parent |
β The read side (
metricsHelper.COLLECTIONS) references the same collection names β this is the contract between cron and API. If the cron does not run, the/statswidgets go stale/empty (see Β§13).
6.2 Legacy / summary collections¶
| Collection | Model | Purpose | Key fields |
|---|---|---|---|
dashboard_mis |
models/dashboardMIS.js β
|
Daily KPI rollup per business-day | created_at, business_id, parent_business_id, total_order_count, total_order_amount, channel counters (total_uen_*, total_swig_*, total_zom_*, total_pos_*, prepaid/COD), new_customer_count, salon/service breakdowns, valid_numbers/invalid_numbers |
business_aggreator |
buisness_aggregator.js β
|
Customer cohort counts (used by Customer Overview) | totalUser, active_user, lost_users, customers_with_orders_lifetime, customers_with_no_orders_lifetime, new_customers_ordered, new_customers_not_ordered |
campaign_mis |
campaign_mis.js β
|
Campaign delivery/ROI MIS | total_campaigns_sent, total_sms_sent, total_sms_price, push/android/ios counters |
dashboard_module |
dashboard_modules.js β
|
Widget registry | display_name, value |
aggregation_process |
π» | Cron run log | started_at, completed_at, status |
rating_mis / mis_rating |
rating_mis.js |
Star Rating Distribution | ratings_done, avg_rating |
digital_bill_mis |
β | Digital Bill Insights | click/view/download counters |
6.3 Relationships, keys, indexes¶
- Tenant keys: every collection is scoped by
parent_business_id(+business_id/child_business_id). β οΈ Type inconsistency β some docs store these asNumber, others asString.metricsHelper.parentQuery()handles this by tryingNumber(x)first, and the summary doesfindOne({businessId:String(x)}) || findOne({businessId:Number(x)}). β - Customer key:
mobile_number(10-digit) linksordersβcustomersβratings. - Indexes: β οΈ Most ETL/MIS collections have no explicit indexes (map-03 Β§5.1). ETL reads filter on a
datefield range +parentBusinessId; add compound indexes on(parentBusinessId, date)if aggregation queries slow down.
6.4 Common queries¶
// Modern: fetch a day-range ETL slice (metricsHelper.fetchRange)
db.collection("orders_by_day_business").find({
date: { $gte: qStart, $lte: qEnd },
parentBusinessId: Number(parentBusinessId)
}).toArray();
// Cron write (aggregationProcess) β idempotent upsert of a materialized row
db.collection("hour_of_day_heatmap_by_parent").replaceOne(
{ parentBusinessId, date }, doc, { upsert: true });
// Customer cohort block for Customer Overview
db.collection("business_aggreator").findOne({ businessId: String(parentBusinessId) });
7. APIs¶
All /stats/* endpoints are POST, guarded by authMiddleware β
, and take { parent_business_id, business_id, start, end } (or date for single-day). Header: token: <staff token> + body userId, business_id.
| Route | Method | Auth | Body | Feeds | Verify |
|---|---|---|---|---|---|
/stats/dashboard/summary |
POST | authMiddleware | parent_business_id, business_id, start, end, date |
All summary widgets | β representative |
/stats/metrics/orders |
POST | authMiddleware | β¦start,end |
Orders Overview | β |
/stats/metrics/fulfillment |
POST | authMiddleware | β¦start,end |
Where Orders Come From | β |
/stats/metrics/outlets |
POST | authMiddleware | β¦start,end |
Filter by Outlet | β |
/stats/metrics/hour-heatmap |
POST | authMiddleware | β¦date |
When Do They Buy | β |
/stats/metrics/meal-breakdown |
POST | authMiddleware | β¦start,end |
Meal-time split | β |
/stats/metrics/items |
POST | authMiddleware | β¦start,end,top |
Item table | β |
/stats/metrics/campaigns |
POST | authMiddleware | β¦start,end |
Campaign block | β |
/stats/metrics/geo |
POST | authMiddleware | β¦start,end |
Geo distribution | β |
/stats/metrics/top-items |
POST | authMiddleware | β¦start,end,top |
Most Selling Item | β |
/stats/metrics/special-days |
POST | authMiddleware | β¦start,end |
Special-days perf | β |
/stats/metrics/underperforming |
POST | authMiddleware | β¦start,end |
Underperforming items | β |
/crm_api/v2/dashboarData |
POST | none β οΈ | (business params) | Legacy v2 dashboard | β
(typo route, businessController.dashboardData, no auth) β see Admin-Business |
7.1 Representative payload β POST /stats/dashboard/summary¶
Request
POST /stats/dashboard/summary
Content-Type: application/json
token: <staff-api-token>
{ "userId": "9812345678", "business_id": 501, "parent_business_id": 42,
"start": "2026-06-01", "end": "2026-06-30" }
Response (shape β
β fields verified in getDashboardSummary)
{
"orders": { "totalOrders": 1240, "totalRevenue": 486000.5, "AOV": 391.94, "series": [...] },
"fulfillment": { "dinein": 0, "delivery": 0, "pickup": 0, "incar": 0, "other": 0 },
"customers": { "total": 8400, "new": null, "returning": null, "active": null, "lost": null, "recovered": null },
"hour": { "peakHours": [13, 20, 21], "buckets": [...] },
"items": [...], "campaigns": {...}, "geo": [...], "specialDays": [...], "underperforming": [...]
}
7.2 Validation & failures¶
metricsHelper.validateRangeParamsrejects missing/malformed dates (YYYY-MM-DD) and missingparent_business_id. β- Dates parsed as IST (
T00:00:00+05:30); a range end is extended toend + 24h - 1ms. β authMiddlewareβ403on bad token,500fail-closed. β- Empty ETL collection β widget returns zeros/empty arrays, not an error (so a missing cron run looks like "no data", not a crash) β οΈ.
8. Code Walkthrough¶
8.1 Read path (/stats)¶
routes/stats.js
β metricsController.getDashboardSummary(req,res) β
β metricsHelper.validateRangeParams(req,res)
β metricsHelper.connectMongo()
β Promise.all([
aggregateOrdersRange, aggregateFulfillmentRange, aggregateOutletsRange,
aggregateHourHeatmapRange, aggregateMealBreakdownRange, aggregateItemsRange,
aggregateCampaignsRange, aggregateGeoRange, aggregateSpecialDaysRange,
aggregateUnderperformRange ]) β each calls fetchRange(COLLECTIONS.x, β¦)
β totalOrders / totalRevenue / AOV / peakHours
β business_aggreator lookup β customersBlock
Each single-metric endpoint (getOrders, getHourHeatmap, getTopItems, β¦) delegates to the matching metricsHelper.aggregate*Range function and returns just that slice. β
metricsHelper key functions (all β
exported):
parseDateStrict, dateRangeFromParams, parentQuery, fetchRange, enrichOutletsWithConfigs (joins outlet ids β names from business_configs), and the aggregate*Range family.
8.2 Write path (cron)¶
aggregationController.js cron "0 0 * * *"
β mark aggregation_process { status:false }
β business_configs.find({ is_parent:true, business_status:1, prism_active:1 })
β aggregationProcess.processAggregationParent(parents) β
β aggregationProcess.processAggregationChild(childIds) β
β mark aggregation_process { status:true } + email
aggregationProcess reads orders, order_items, customers, customers_outlets, business_configs, promotions and writes 14 collections via replaceOne(upsert) / bulkWrite (idempotent β re-runnable for the same day). β
IST date math throughout (ETL_DATE_FORMAT = "%Y-%m-%d %H:%M:%S").
8.3 Modern dashboard controller¶
dashboardControllerNew.js β
owns the business checklist / onboarding progress (checkListAPI), KPI cards, graph/canvas image generation (AWS), and uses Redis for caching hot dashboard payloads and Socket.io for pushing real-time updates to the open dashboard. (Full per-function detail: verify in code β file is ~2560 lines.)
Dependencies: utilities/mongodb, utilities/redis, socket.js, logger, AWS SDK (image gen), axios.
9. Business Rules (calc logic + cross-widget integrity)¶
9.1 Widget β calculation β data source (π Excel logic, mapped to code)¶
| Widget | Calculation π | Data source |
|---|---|---|
| Smart Alert Banners | Anomaly thresholds vs prior period + low-wallet | daily_summary_by_parent + business_configs |
| Filter by Outlet | Scope all widgets to one business_id |
outlet_metrics_by_day |
| Customer Overview | Total Customers = Transacting + Non-Transacting |
business_aggreator |
| New vs Returning | New = first order in period; Returning = ordered before AND in period | orders_by_day_business / business_aggreator |
| Where Orders Come From | Channel revenue sums to 100%; each channel share | fulfillment_breakdown_by_parent, dashboard_mis channel counters |
| Orders Overview | Total Orders = Delivery + Pick-Up + Dine-In |
orders_by_day_business, fulfillment_breakdown_by_parent |
| When Do They Buy | Orders bucketed by hour (outlet TZ / IST) | hour_of_day_heatmap_by_parent |
| Star Rating Distribution | Count per star 1β5, avg_rating |
rating_mis |
| Upcoming Birthdays | Customers with birthday in next N days | customers (KYC birthday) |
| Digital Bill Insights | Sent / viewed / downloaded / feedback | digital_bill_mis |
| Feedback Insights | Rating + NPS aggregates | rating_mis, feedback collections |
| Loyalty Analysis | Total Points = Used + Available + Expired (never negative) |
wallet/loyalty collections |
| Most Selling Item | Top items by qty/revenue (excl. cancelled) | top_selling_items_by_day |
| Your Effective Offers | Offer ROI; AOV = Revenue / Redemptions | promotions / campaign_metrics_by_day |
9.2 Cross-widget integrity rules (first-class concern β Roadmap A4.4) π¶
These invariants must hold across every widget; a violation is a P0 data bug:
Total Customers = Transacting + Non-TransactingTotal Orders = Delivery + Pick-Up + Dine-InTotal Loyalty Points = Used + Available + Expired(never negative)- Channel revenue shares sum to 100%
AOV = Revenue / Orders(and for offers,Revenue / Redemptions)Unique Customers β€ Redemptionsalways
β οΈ In the modern summary,
AOVis computed astotalRevenue / totalOrderswith a divide-by-zero guard (totalOrders ? β¦ : 0). β The Effective Offers widget must useRevenue / Redemptionsβ see bug E1.9 below.
9.3 Data Validation Protocol (P0, MANDATORY per migrated screen) π¶
Every Next.js-migrated widget must pass Roadmap A4.1βA4.6: logic validation (matches Product Logic Spec), composition (breakdowns sum), sync time (orders <5min, profiles <5min, points real-time, campaign stats <15min, RFM nightly), cross-widget integrity, regression on merges, period-filter consistency. This is the acceptance bar for any dashboard PR.
9.4 Known bugs correlated to this module β οΈ (Roadmap Β§E)¶
| Bug | Symptom | Root-cause / fix direction |
|---|---|---|
| E1.9 (P1) | "Your Effective Offers" shows AOV = βΉ0 | AOV wrongly divides by Orders (or a 0 denominator); must be Revenue / Redemptions. Fix in the effective-offers calc. |
| E1.10 (P1) | Loyalty 2-point discrepancy: Total β Used + Available + Expired |
Points live across MongoDB and MySQL, non-atomic (see High-Level Arch Β§9); reconcile the ledger. Affects Loyalty Analysis widget. |
| E1.1 (P0) | ROI calculation wrong | campaignMis.js / campaign_metrics_by_day attribution window logic. Affects campaign + Effective Offers widgets. |
| E1.4 (P0) | Various CRM statistics incorrect | dashboard_mis / business_aggreator rollups drift; validate against A4 protocol. |
Full triage: ../../13-CommonIssues/Known-Bugs-From-Roadmap.md.
10. Performance¶
- Materialized views: the modern path pushes all
GROUP BYto the nightlyaggregationProcesscron (0 0 * * *IST). Read endpoints only scan pre-shaped, day-keyed docs β O(days) instead of O(orders). β - Parallelism:
getDashboardSummaryruns 10 aggregations inPromise.all. β - Redis cache:
dashboardControllerNewcaches hot payloads (ElastiCache serverless). β - Idempotent writes: ETL uses
replaceOne(upsert)keyed on(parentBusinessId, date)β safe to re-run for a given day. - MongoDB read preference:
SECONDARY_PREFERRED, maxPool 400 (infra) β analytics reads offload to replicas. - β οΈ Missing indexes on ETL collections (
(parentBusinessId, date)); large ranges may table-scan. - β οΈ Cron coupling: 91 crons run in-process with the web server β a heavy aggregation night can raise API latency.
11. Logging¶
- Winston (
require('./logger')) βlogger.info/error; daily-rotatingcombined-*.log/error-*.log. β aggregationControllerlogs run start/finish and sends email alerts on start/complete/failure. β- Trace correlation:
loggerService+traceIdMiddlewareinject atrace_idper request (infra Β§5.2). pool_statsJSON every 60s for MySQL pool health (relevant when legacy dashboard hits MySQL).
12. Monitoring¶
- Cron health:
aggregation_processcollection β a run withstatus:falseand nocompleted_atolder than ~1 day = stuck/failed aggregation. Watch the start/complete emails. - Data freshness: compare latest
dateinorders_by_day_businessvs today; a gap = cron missed. - Sync-time SLOs (A4.3): orders <5min, profiles <5min, points real-time, campaign stats <15min, RFM nightly.
- APM: New Relic / Elastic present but disabled (infra Β§5.4).
13. Troubleshooting¶
| Issue | Likely cause | Fix | Command |
|---|---|---|---|
/stats widgets empty/stale |
Aggregation cron didn't run | Re-run aggregation; check run log | db.aggregation_process.find().sort({started_at:-1}).limit(3) |
| Widget shows 0 for a live channel | Integration inactive OR cron gap | Confirm channel counter in dashboard_mis; verify integration |
db.dashboard_mis.find({business_id:501}).sort({created_at:-1}).limit(1) |
| AOV = βΉ0 in Effective Offers | Bug E1.9 β wrong denominator | Use Revenue/Redemptions in offers calc |
grep effective-offers calc |
| Loyalty totals off by 2 | Bug E1.10 β Mongo/MySQL ledger drift | Reconcile points ledger | see Loyalty module |
| Numbers differ between two widgets | Cross-widget integrity break (A4.4) / type mismatch on parent_business_id |
Verify all widgets read same period + same tenant type | check parentQuery coercion |
/stats returns 403 |
Bad staff token | Re-auth | verify token header + business_id |
| Slow summary | Missing index / huge range | Add (parentBusinessId,date) index; narrow range |
db.orders_by_day_business.getIndexes() |
/crm_api/v2/dashboarData unauthenticated data |
Route has no middleware β οΈ | Add authMiddleware (see Admin-Business) |
routes/crmv2.js |
14. FAQs¶
- Legacy vs modern β which is source of truth? The modern
/stats+aggregationProcesspath is the target; the legacydashboardController/dashboard_mispath is being deprecated. New widgets should read ETL collections. β»οΈ - Why does a widget show 0 instead of hiding the channel? By design (Excel 3.5) β an inactive channel must appear as 0, not vanish. β
- Where does AOV come from?
totalRevenue / totalOrdersingetDashboardSummary; the offers widget must useRevenue / Redemptions(E1.9). β - Do the read APIs write anything? No β read-only. Only the cron writes. β
- Why are counts sometimes matched with both Number and String ids? Tenant-id type inconsistency in Mongo;
metricsHelpercoerces defensively. β οΈ - What triggers aggregation?
node-cron "0 0 * * *"inaggregationController, over active parents. β
15. Cheat Sheet¶
READ (modern): POST /stats/dashboard/summary { parent_business_id, business_id, start, end }
POST /stats/metrics/{orders|fulfillment|outlets|hour-heatmap|meal-breakdown|
items|campaigns|geo|top-items|special-days|underperforming}
CONTROLLER: metricsController.* β commonFunctions/metricsHelper.aggregate*Range
WRITE (cron): aggregationController "0 0 * * *" IST β aggregationProcess.processAggregation{Parent,Child}
ETL COLLS: orders_by_day_business, fulfillment_breakdown_by_parent, outlet_metrics_by_day,
outlet_toplist_by_day, state_city_distribution_by_day, hour_of_day_heatmap_by_parent,
meal_time_breakdown_by_parent, campaign_metrics_by_day, top_selling_items_by_day,
item_revenue_contribution_by_day, product_pairing_by_day, category_performance_by_day,
underperforming_items_by_day, daily_summary_by_parent
LEGACY: dashboardController (dashboard_mis, rating_mis) β°οΈ
INTEGRITY: Customers = Transacting+Non-Transacting | Orders = Delivery+PickUp+DineIn
Points = Used+Available+Expired | Channel rev = 100% | AOV = Rev/Orders (offers: Rev/Redemptions)
BUGS: E1.9 AOV=βΉ0 (offers) | E1.10 loyalty Β±2 | E1.1 ROI | E1.4 CRM stats
RUN LOG: db.aggregation_process (status:false = running/stuck)
Related Modules¶
- Customer-Intelligence-RFM β feeds Customer Overview / New-vs-Returning segments.
- Campaigns β
campaign_mis, ROI attribution (E1.1) powering the campaign widgets. - Loyalty β Loyalty Analysis widget + point-integrity bug E1.10.
- Feedback-NPS β Star Rating & Feedback Insights (
rating_mis). - Admin-Business β parent-level admin dashboards +
/crm_api/v2/dashboarData. - Database Β· Queues Β· Known Bugs Β· Roadmap
Knowledge Tests¶
Level 1 β MCQs¶
1. Which path does POST /stats/dashboard/summary use to get its numbers?
A) Live GROUP BY on orders per request B) Pre-aggregated ETL collections built by a nightly cron C) Redis only D) MySQL addo_orders
Answer: B. metricsHelper.aggregate*Range read materialized collections (orders_by_day_business, etc.) written by aggregationProcess.
2. What schedule runs the aggregation ETL?
A) every 5 min B) 0 0 * * * (midnight IST) C) hourly D) on every API call
Answer: B. aggregationController uses cron.schedule("0 0 * * *"); dates handled in IST.
3. The cross-widget rule for orders is:
A) Orders = New + Returning B) Orders = Delivery + Pick-Up + Dine-In C) Orders = Prepaid + COD D) Orders = Swiggy + Zomato
Answer: B. π Integrity rule; matches fulfillment_breakdown_by_parent.
4. Bug E1.9 is about: A) ROI window B) 2-point loyalty gap C) AOV showing βΉ0 in Effective Offers (should be Revenue/Redemptions) D) missing indexes Answer: C.
5. Why can the same tenant be looked up as both Number and String?
A) A/B test B) Mongo stores parent_business_id inconsistently across collections C) Redis quirk D) Sharding
Answer: B. β οΈ Type inconsistency; metricsHelper.parentQuery + summary fallbacks coerce defensively.
Level 2 β Scenarios¶
S1. A brand reports "orders show 0 for Zomato since Tuesday, but Zomato is live." Walk through diagnosis.
Expected: Check aggregation_process for a missed/stuck run since Tuesday; check latest date in orders_by_day_business and Zomato counters in dashboard_mis; confirm the Zomato integration is active in business_configs; if the cron gapped, re-run aggregation for the missing days (idempotent upsert). Remember: an inactive channel must show 0 by design, so distinguish "integration down" from "cron gap".
S2. QA says Customer Overview "Total" β Transacting + Non-Transacting for one parent. What broke and what protocol applies?
Expected: Cross-widget/composition integrity break (A4.2/A4.4). Likely business_aggreator cohort fields stale or a tenant-id type mismatch splitting the count across Number/String docs. Fix the rollup + apply the Data Validation Protocol before sign-off.
Level 3 β Code reading¶
Read getDashboardSummary in metricsController.js. (a) How is AOV guarded against divide-by-zero? (b) Why are there two business_aggreator.findOne calls chained with ||? (c) How are peakHours derived?
Expected: (a) totalOrders ? Math.round(totalRevenue/totalOrders*100)/100 : 0. (b) tenant-id stored as String in some docs, Number in others β try both. (c) sort hourBuckets by orders desc, take top 3 .hour.
Level 4 β Architecture¶
Prism is moving dashboards from the legacy per-request model to the pre-aggregated /stats model. Explain the trade-off (freshness vs latency/scale), why aggregation_process + idempotent replaceOne matter, and one risk of the in-process cron design.
Expected: Materialized views trade real-time freshness (data is as fresh as the last nightly run) for fast, scalable reads; run-tracking + idempotency make the ETL safe to retry/backfill; risk: 91 in-process crons couple cron load to API latency (a heavy aggregation night slows requests).
Level 5 β Prod debugging¶
Effective Offers shows AOV βΉ0 for every brand; other widgets are fine. Diagnose and fix.
Expected: This is E1.9. Other widgets compute AOV = Revenue/Orders correctly; the offers widget must use Revenue/Redemptions. Locate the effective-offers calc (offers/promotions path), confirm the denominator, add a divide-by-zero guard, and re-validate against the integrity rule Unique Customers β€ Redemptions. Verify no cancelled-order revenue leaks in.
Practical Assignments¶
-
Trace a widget end-to-end. Pick "When Do They Buy". Find where
hour_of_day_heatmap_by_parentis written (aggregationProcess.js) and read (aggregateHourHeatmapRangeβgetHourHeatmap). Document the exact fields and the IST bucketing. Deliverable: a one-page data-lineage note. -
Reproduce & fix E1.9 locally. Seed a small
promotions/offers dataset with known redemptions, hit the effective-offers path, confirm AOV = βΉ0, then patch the denominator toRevenue/Redemptionsand prove the integrity rules hold. Deliverable: before/after numbers + the diff. -
Add a freshness monitor. Write a script that compares the max
datein each ETL collection againsttoday-1and the latestaggregation_processstatus, and emits an alert line per stale collection. Deliverable: the script + sample output (fits the Monitor pattern in Queues).