Feedback-NPS Module¶
One-line: After an order (usually via the Digital Bill link), the customer sees a brand-customizable feedback form → a star rating + NPS score + review text is captured, high ratings are routed to Google/Zomato/Apple reviews while low ratings trigger a recovery journey (promo + merchant alert), and external review streams (Google Play / Apple / aggregators; GMB planned) are aggregated into
ratings_reviews/rating_mis.
Validation legend¶
| Symbol | Meaning |
|---|---|
| ✅ | Verified in code this review |
| 📄 | From PM Excel / business docs |
| 💻 | Code detail confirmed |
| ♻️ | Duplicated / overlapping implementation |
| ⚰️ | Legacy |
| ⚠️ | Bug, risk, or footgun |
1. Overview¶
Feedback-NPS is Prism's customer sentiment layer (📄 Sub Products & Microsites; Roadmap A2.3 merges Reviews + NPS → "Customer Feedback & Sentiment"). It spans three surfaces:
- First-party feedback ✅ — form delivered via SMS/WhatsApp (
feedbackFormFlow.js), captured inratings_reviews(source="3"feedback form). - NPS ✅ — Net Promoter scoring stored per-business (
nps_{business_id}) with aggregates innps_aggreator; config innps_config, delivery queue innps_comm_request. - Review aggregation ✅ — Google Play (
googleReviews.js→google_reviews), Apple App Store (appleReviews.js→ios_reviews), plus internal order-level ratings; GMB (Google My Business) planned 📄.
NPS buckets 📄 (Terminologies): Promoter 9–10 / Passive 7–8 / Detractor 0–6. ⚠️ These buckets are not hard-coded as constants in the reviewed code — npsData converts a ≤5 answer to a 0–10 scale (answer*2); bucketing is applied at report/UI layer (verify in the Next.js frontend / nps_aggreator computation).
⚠️ Related bug E1.7 — "All 8 automated journey logics need verification" (P0) 📄: the Low Rating Rewards journey lives here (
lowRatingTrigger.js) and is in scope. Review aggregation is also flagged loosely under CRM-stats accuracy (E1.4).
2. Business Flow¶
Example journey¶
- Digital Bill / feedback SMS delivers the form link (JWT-signed) to the customer.
- Customer submits star rating + answers → stored in
ratings_reviews(source="3"). - High rating → form routes the customer to leave a public review (Google/Zomato) 📄.
- Low rating →
lowRatingTrigger.sendPromoForLowRatingTriggerchecks the journey config (campaign type67163ab08a4992e9d601429e), and ifrating ≤ thresholdandrating_frequency < frequency_rating, sends a recovery promo (SMS/WhatsApp) + a merchant alert. - Nightly,
npsDataconverts eligible feedback into NPS records;googleReviews/appleReviewspull public reviews every 3 hours.
Edge cases ✅¶
- Rating frequency cap — a customer is only re-triggered for low-rating recovery while
rating_frequency < config.frequency_rating(rating_frequencycounter incremented per send). - Special parent 7175 — bespoke direct-send path (
sendTemplate) + a legacyfeed_back_sms_queue;npsDatacron is hardcoded to parent 7175 only ⚠️. - Special parent 31763 — feedback delay 20 min (POS) vs 90 min (app/website) ⚠️.
- Insufficient wallet — recovery/alert sends blocked (wallet + GST checked).
- Review dedup — Google/Apple syncs upsert by
reviewId, updating only on changed text/reply.
3. Technical Flow¶
- Source ✅ — feedback form submission (from Digital Bill link), NPS trigger, external review APIs (Google Play Publisher, App Store Connect).
- API ✅ — feedback/nps/rating read routes (
authMiddleware);POST /nps/data(no auth) ingests form ratings. - Validation ✅ — Indian mobile
[6-9]\d{9}; journey threshold + frequency checks. - Logic ✅ —
feedbackFormFlow.processSmsFeedback(send form),lowRatingTrigger(recovery + alert),npsData(convert to NPS), review crons (pull + upsert). - DB ✅ —
ratings_reviews,rating_mis/rating_count_mis,nps_{business_id},nps_aggreator,google_reviews,ios_reviews,feedback_customers,feedback_mis,feedback_configs. - Events / Queue ✅ — recovery/feedback sends →
sms_queue_YYYYMMDD/whatsapp_queue_YYYYMMDD; legacyfeed_back_sms_queue(parent 7175). - Notification ✅ — recovery promo to customer; alert (SMS/WhatsApp/email) to merchant.
- Response ✅ — feedback URL from
POST https://www.uengage.in/Feedback_form/sendFeedbackSms; wallet debit logged inwallet_transactions.
4. Architecture Diagram¶
Flowchart¶
flowchart TD
BILL[Digital Bill / feedback SMS] --> FORM[Feedback form JWT link]
FORM --> SUBMIT[/POST /nps/data - form submit/]
SUBMIT --> RR[(ratings_reviews source=3)]
RR --> HIGH{rating high?}
HIGH -->|yes| PUB[Route to Google / Zomato review 📄]
HIGH -->|no| LRT[lowRatingTrigger]
LRT -->|promo| SQ[(sms_queue / whatsapp_queue)]
LRT -->|merchant alert| ALERT[SMS / WhatsApp / Email]
LRT --> ROI[(auto_campaign_roi + wallet_transactions)]
subgraph Send["Feedback delivery"]
FF[feedbackFormFlow.processSmsFeedback]
FF --> SQ
FF --> FC[(feedback_customers)]
FF --> FM[(feedback_mis)]
end
subgraph NPS["NPS"]
NC[nps_config] --> NCR[(nps_comm_request queue)]
NPSCRON[npsData cron 06:00 IST\nparent 7175 only ⚠️]
RR --> NPSCRON --> NPSREC[(nps_business_id)]
NPSREC --> NAGG[(nps_aggreator)]
end
subgraph Reviews["External review aggregation"]
GR[googleReviews 0 */3]
AR[appleReviews 0 */3]
GR --> GRC[(google_reviews)]
AR --> ARC[(ios_reviews)]
GMB[GMB planned 📄]:::planned
end
classDef planned stroke-dasharray: 4 4;
Sequence — low-rating recovery¶
sequenceDiagram
autonumber
participant CUST as Customer
participant FORM as Feedback form
participant RR as ratings_reviews
participant LRT as lowRatingTrigger
participant CJ as campaign_journeys (type 67163ab0…)
participant Q as sms_queue / whatsapp_queue
participant MERCH as Merchant
CUST->>FORM: submit low rating
FORM->>RR: insert {source:"3", rating}
RR->>LRT: sendPromoForLowRatingTrigger
LRT->>CJ: read config (rating threshold, frequency_rating)
alt rating<=threshold AND rating_frequency<frequency_rating
LRT->>Q: enqueue promo (SMS/WhatsApp) + debit wallet
LRT->>MERCH: sendAlertForMerchantFeedback (SMS/WA/email)
LRT->>RR: increment rating_frequency
else capped / above threshold
LRT-->>RR: no-op
end
5. Folder Structure (real files)¶
uengage-crm/
├── controllers/
│ ├── feedbackController.js ✅ getFeedbackCustomers, ordersSummary, feedbackFormMis
│ ├── npsControllers.js ✅ getNpsData, getNpsList, npsData, downloadData
│ ├── ratingController.js ✅ rating/review aggregation + listings
│ └── lowRatingTrigger.js ✅ low-rating recovery + merchant alert
├── commonFunctions/
│ └── feedbackFormFlow.js ✅ processSmsFeedback (send form SMS/WA)
├── crons/
│ ├── feedback_data_sync.js ✅ parent→child feedback_configs sync (12:06 IST)
│ ├── send_feed_back_sms.js ✅ legacy feed_back_sms_queue drain (every 5m)
│ ├── googleReviews.js ✅ Google Play reviews (every 3h)
│ ├── appleReviews.js ✅ App Store reviews (every 3h)
│ └── npsData.js ✅ feedback→NPS convert (06:00 IST, parent 7175)
└── models/
├── ratings_reviews.js ✅ collection: rating_review
├── rating_mis.js ✅ model: mis_rating
├── nps_config.js ✅ collection: nps_config
└── nps_comm_request.js ✅ collection: nps_comm_request
6. Database¶
Models / collections ✅¶
| Store | Model → Collection | Purpose | Key fields |
|---|---|---|---|
| Mongo | rating_review (models/ratings_reviews.js) |
All ratings/reviews | business_id, parent_business_id, mobile_number, order_id, rating, rating_comments, inserted_at, source ("1" order-level / "2" prism / "3" feedback form), questions[], other_params (e.g. "Hear About Us From"), gender |
| Mongo | mis_rating (models/rating_mis.js) |
Rolled-up rating stats | inserted_at, business_id, parent_business_id, ratings_done, avg_rating |
| Mongo | rating_count_mis |
Datewise rating counts | rating_count, date, parent_business_id, child_business_id |
| Mongo | nps_config (models/nps_config.js) |
NPS survey config | ans_validty, parentBusinessId, request_mode, request_trigger_on, request_trigger_interval, status |
| Mongo | nps_comm_request (models/nps_comm_request.js) |
NPS delivery queue | businnesId (typo ⚠️), parentBusinessId, orderId, source, customer_name, emailId, mobileNo, processed (0/1/2), processTime, insertedAt |
| Mongo | nps_{business_id} |
Per-business NPS records | mobileNo, rating, businessId, questions[], insertedAt, validTill, order_id, status, contactMappingId, parentBusinessId, nps_id |
| Mongo | nps_aggreator |
Parent-level NPS aggregate | pId, businessWise[] |
| Mongo | google_reviews |
Google Play reviews | reviewId, reviewer, comment, reply, starRating, createTime, lastModified, parent_business_id |
| Mongo | ios_reviews |
Apple reviews | id, parent_business_id, appId, attributes (from Apple API) |
| Mongo | feedback_customers / feedback_mis |
Feedback send + funnel | total_feedback_sent, total_clicks, unique_clicks, total_submitted, is_submitted |
| Mongo | feedback_configs |
Feedback flow gate | feedback_flow (0/1), feedback_sms, feedback_whatsapp, feedback_time, templates, feedback_*_alert, alert_contacts, alert_emails |
| Mongo | campaign_journeys |
Low-rating journey config | campaign_type_id, rating (threshold), frequency_rating, promo_details |
Indexes: none defined on these collections ⚠️ (map-03 §5.1).
mobile_number,parent_business_id, andsourceare the hot filters and should be indexed.
Relationships¶
feedback_configs (parent, child_business_id="0") → synced to child via feedback_data_sync. Feedback submissions → ratings_reviews → feed both NPS (npsData) and low-rating recovery (lowRatingTrigger reads campaign_journeys). External reviews are independent streams keyed by parent_business_id.
Common queries ✅¶
// feedback-form ratings for a parent yesterday
db.rating_reviews.find({ parent_business_id:"7175", source:"3", inserted_at:{ $gte, $lt } })
// how-did-you-hear breakdown
db.rating_reviews.aggregate([{ $group:{ _id:"$other_params.Hear About Us From", n:{$sum:1} } }])
7. APIs¶
Auth = authMiddleware (POST) unless noted.
| Route | Auth | Purpose | Function |
|---|---|---|---|
/fetch_feedback_customers |
✅ | Paginated feedback list (20/page) | feedbackController.getFeedbackCustomers |
/orders_summary |
✅ | Order + valid/invalid-number summary | feedbackController.ordersSummary |
/feedback_form_interaction_mis |
✅ | Feedback funnel (sent/clicks/submissions) | feedbackController.feedbackFormMis |
/npsData |
✅ | Aggregated NPS (nps_aggreator) |
npsController.getNpsData |
/npsList |
✅ | Paginated NPS responses (20/page) | npsController.getNpsList |
/nps/data |
⚠️ no auth | Ingest form rating → NPS record | npsController.npsData |
/download/nps/data |
✅ | XLSX export | npsController.downloadData |
/get/rating_count_datewise |
✅ | Datewise counts | ratingController.ratingReviewsCount |
/get/hear_about_us_source |
✅ | Acquisition-channel breakdown | ratingController.getHowDidYouHearData |
/get/rating_list |
✅ | Reviews across sources (paginated) | ratingController.fetchRatingListing |
/get/latest_rating |
✅ | Latest 5★ reviews (comment ≥100 chars) | ratingController.latestRatingDetails |
Rating source mapping ✅ (fetchRatingListing): 1/2 = internal (rating_reviews), 3 = Google (google_reviews), 4 = Apple (ios_reviews).
Representative example — NPS list (verify)¶
// POST /npsList Headers: { token: <auth> }
{ "business_id":"7175","page":1,"rating":6,"mobile_number":"9876543210" }
// 200 → { "status":1, "data":[ { "mobileNo":"98…", "rating":6, "questions":[…] } ], "total": 42 }
business_id; empty page → data:[].
8. Code Walkthrough¶
Feedback delivery ✅ — feedbackFormFlow.processSmsFeedback:
validate mobile [6-9]\d{9}
├─ parent 7175 → direct sendTemplate (uengage) ⚰️
└─ else read feedback_configs (child feedback_flow==1 else parent)
├─ SMS (feedback_sms=="true") → sms_queue_YYYYMMDD (cost 0.14 + GST)
├─ WA (feedback_whatsapp=="true") → whatsapp_queue_YYYYMMDD (utility 0.17 / mktg 0.91 + GST)
├─ feedback URL: POST Feedback_form/sendFeedbackSms (JWT token)
└─ insert feedback_customers + upsert feedback_mis
Low-rating recovery ✅ — lowRatingTrigger:
sendPromoForLowRatingTrigger
├─ read campaign_journeys {campaign_type_id:"67163ab08a4992e9d601429e"}
├─ gate: campaignConfig.rating >= order_rating AND rating_frequency < frequency_rating
├─ generatePromo() (MySQL addo_promo_code_engine) → coupon
├─ sendWhatsappAndSmsPromo() → sms_queue / whatsapp_queue + wallet debit + auto_campaign_roi
├─ sendAlertForMerchantFeedback() → SMS/WA/email to alert_contacts/alert_emails
└─ increment rating_frequency (customers / customers_outlets)
update_template(): ue_cust_name, ue_cust_mobile, ue_cust_eid, ue_dynamic_coupon, ue_jwt_token
NPS conversion ✅ — crons/npsData.js (06:00 IST, parent 7175 only ⚠️): reads rating_reviews source="3" for yesterday, $lookup customers/orders, converts answer ≤5 → answer*2 (0–10 scale), resolves contactMappingId (MySQL), inserts into nps_7175 with validTill = +365 days.
Review aggregation ✅:
- googleReviews (0 */3 * * *) — Google Play Publisher API via service account (business 1 creds), paginated (maxResults 200), upsert google_reviews by reviewId, update google_last_reviewed.
- appleReviews (0 */3 * * *) — App Store Connect API, JWT auth (key from S3), customerReviews endpoint, bulkWrite upsert into ios_reviews, update apple_last_reviewed.
Config sync ✅ — feedback_data_sync (06 12 * * *) replicates parent feedback_flow=1 config to each outlet in feedback_outlets → child feedback_configs.
Legacy ⚰️ — send_feed_back_sms (*/5 * * * *) drains feed_back_sms_queue (parent 7175 only): processed 0→2 (picked)→1 (done), executes stored URL via GET.
Dependencies: axios, jsonwebtoken (form + button JWTs), xlsx (NPS/report export), googleapis (Play), Apple JWT, MySQL (addo_contacts/addo_promo_code_engine).
9. Business Rules¶
| Rule | Value ✅ | Source |
|---|---|---|
| Feedback form source tag | source="3" |
ratings_reviews |
| Low-rating journey type | 67163ab08a4992e9d601429e |
lowRatingTrigger |
| Low-rating gate | rating ≤ config.rating AND rating_frequency < config.frequency_rating |
✅ |
| NPS scale conversion | answer ≤5 → answer*2 |
npsData |
| NPS buckets 📄 | Promoter 9–10 / Passive 7–8 / Detractor 0–6 | ⚠️ not hard-coded in reviewed code (verify frontend) |
| NPS validity | +365 days | npsData |
| Feedback SMS cost | ₹0.14 + 18% GST | wallet_config |
| Feedback WhatsApp cost | ₹0.17 utility / ₹0.91 marketing + 18% GST | wallet_config |
| Feedback delay | feedback_time min (parent 31763: 20/90 ⚠️) |
feedbackFormFlow |
| Review sync | Google + Apple every 3h | crons |
| Config sync | parent→child 12:06 IST | feedback_data_sync |
| Pagination | 20/page (feedback & NPS) | controllers |
Hidden logic ⚠️: npsData + feed_back_sms_queue are parent-7175-specific; per-parent delay override for 31763; JWT secrets embedded in lowRatingTrigger/feedbackFormFlow (security-review item). Merchant service_id mapping: 1=SMS, 4=WhatsApp, 10=Email.
GMB (Google My Business) 📄 — a planned review source (not yet a cron/collection); track alongside Google Play (google_reviews) and Apple (ios_reviews). See Roadmap.
10. Performance¶
- Review crons paginate external APIs (200/page) and upsert — network-bound, safe cadence (3h).
npsDatauses Mongo$lookup+ MySQL round-trips per record ⚠️ — fine at parent-7175 scale, would need batching if generalized.- Hot filter fields on
ratings_reviewsare unindexed ⚠️ — listing/aggregation degrade with volume.
11. Logging¶
- Winston standard; wallet debits in
wallet_transactions; recovery sends logged inauto_campaign_roi. - Review crons update
*_last_reviewedmarkers as a progress log.
12. Monitoring¶
- Feedback funnel:
feedback_mis(sent → clicks → submissions). - NPS trend:
nps_aggreator. - Journey health (E1.7): audit that low-rating triggers actually enqueue and respect the frequency cap.
13. Troubleshooting¶
| Issue | Cause | Fix | Command |
|---|---|---|---|
| Low-rating customer keeps getting promos | frequency_rating too high / counter not incrementing |
Verify rating_frequency vs config.frequency_rating |
db.customers_999.findOne({mobileNo},{rating_frequency:1}) |
| No recovery sent on low rating (E1.7) | Threshold/config gate failed, or journey disabled | Check campaign_journeys config |
db.campaign_journeys.findOne({campaign_type_id:"67163ab08a4992e9d601429e"}) |
| NPS empty for a brand | npsData only runs for parent 7175 |
Confirm parent id / generalize cron | inspect crons/npsData.js |
| Google reviews not updating | service-account creds / package_name missing |
Verify business config | check google_last_reviewed |
| Feedback form not delivered | feedback_flow off or wallet low |
Check config + balance | db.feedback_configs.findOne({parent_business_id}) |
14. FAQs¶
- Where do the NPS buckets live? Conceptually Promoter/Passive/Detractor 📄; code stores raw score and converts ≤5 to a 0–10 scale — bucketing is at report/UI layer ⚠️.
- High vs low rating routing? High → public review (Google/Zomato) 📄; low →
lowRatingTriggerrecovery. - Which review sources exist today? Google Play + Apple + internal order-level; GMB planned 📄.
- Is
/nps/dataauthenticated? No ⚠️ — it ingests form ratings.
15. Cheat Sheet¶
feedback source tag : ratings_reviews.source = "3"
NPS buckets 📄 : Promoter 9-10 · Passive 7-8 · Detractor 0-6 (not const in code ⚠️)
low-rating journey : campaign_type_id 67163ab08a4992e9d601429e
low-rating gate : rating<=config.rating AND rating_frequency<frequency_rating
review crons : googleReviews & appleReviews (0 */3 * * *)
nps cron : npsData 06:00 IST (parent 7175 only ⚠️)
config sync : feedback_data_sync 12:06 IST (parent→child)
cost : SMS ₹0.14 · WA util ₹0.17 / mktg ₹0.91 (+18% GST)
review sources : Google Play · Apple · internal · (GMB planned)
KNOWN BUG : E1.7 journey logic verification (low-rating recovery)
Related Modules¶
- Digital-Bills — delivers the feedback link inside every bill.
- Automated-Journeys — the Low-Rating-Rewards journey is one of the 8.
- WhatsApp / SMS-Push — recovery + alert delivery channels.
- Campaigns — shares wallet/queue +
auto_campaign_roi. - Queues · Third-Party · Database · Roadmap E1.7
Knowledge Tests¶
Level 1 — MCQs¶
- A feedback-form rating is stored with source = a) "1" b) "2" c) "3" d) "4" — c.
- NPS Detractor range is a) 0–6 b) 7–8 c) 9–10 d) 1–3 — a 📄.
- The low-rating journey re-triggers only while a) wallet>0 b) rating_frequency < frequency_rating c) rating>4 d) always — b.
- Apple reviews are stored in a) google_reviews b) ratings_reviews c) ios_reviews d) nps_aggreator — c.
npsDatacron currently runs for a) all parents b) parent 7175 only c) child businesses d) none — b ⚠️.
Level 2 — Scenarios¶
- A brand complains customers who gave 2★ never got the recovery offer. Walk through the three gate conditions in
lowRatingTriggeryou'd check, in order. (Expect: journey config exists/enabled → rating ≤ threshold → rating_frequency < frequency_rating; plus wallet balance.) - NPS dashboard shows all Detractors as "6" for a brand that uses a 1–5 star form. Explain how the
answer*2conversion produces this and whether it's a bug. (Expect: 3★→6 via ≤5 conversion; expected behavior, but bucketing/label may mislead — verify frontend.)
Level 3 — Code reading¶
In npsData.js, a feedback answer of 5 and an answer of 8 arrive. What rating is written for each into nps_7175? (Answer: 5≤5 → 10; 8>5 → 8.)
Level 4 — Architecture¶
npsData and the feed_back_sms_queue path are hardcoded to parent 7175. Design how to generalize NPS to all parents using nps_config/nps_comm_request (already modeled) and note the batching/index changes required.
Level 5 — Prod debugging¶
Merchants report a flood of duplicate low-rating alerts. Given the frequency cap is per-customer via rating_frequency, list the two most likely root causes and the command to confirm whether the counter is incrementing.
Practical Assignments¶
- Write a read-only audit that verifies, for a sample of low ratings yesterday, whether a recovery send was enqueued and
rating_frequencyincremented (directly probing E1.7). - Propose the minimal index set for
ratings_reviews(parent_business_id,source,inserted_at,mobile_number) with justification per query in §6. - Draft the design (no code) to add GMB as a third review-aggregation cron mirroring
googleReviews.js, including collection shape and dedup key.