Skip to content

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:

  1. First-party feedback ✅ — form delivered via SMS/WhatsApp (feedbackFormFlow.js), captured in ratings_reviews (source="3" feedback form).
  2. NPS ✅ — Net Promoter scoring stored per-business (nps_{business_id}) with aggregates in nps_aggreator; config in nps_config, delivery queue in nps_comm_request.
  3. Review aggregation ✅ — Google Play (googleReviews.jsgoogle_reviews), Apple App Store (appleReviews.jsios_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

  1. Digital Bill / feedback SMS delivers the form link (JWT-signed) to the customer.
  2. Customer submits star rating + answers → stored in ratings_reviews (source="3").
  3. High rating → form routes the customer to leave a public review (Google/Zomato) 📄.
  4. Low ratinglowRatingTrigger.sendPromoForLowRatingTrigger checks the journey config (campaign type 67163ab08a4992e9d601429e), and if rating ≤ threshold and rating_frequency < frequency_rating, sends a recovery promo (SMS/WhatsApp) + a merchant alert.
  5. Nightly, npsData converts eligible feedback into NPS records; googleReviews/appleReviews pull 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_frequency counter incremented per send).
  • Special parent 7175 — bespoke direct-send path (sendTemplate) + a legacy feed_back_sms_queue; npsData cron 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 → API → Validation → Logic → DB → Events → Queue → Notification → Response
  • 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; legacy feed_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 in wallet_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, and source are 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 }
Failures: 400 missing 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).
  • npsData uses Mongo $lookup + MySQL round-trips per record ⚠️ — fine at parent-7175 scale, would need batching if generalized.
  • Hot filter fields on ratings_reviews are unindexed ⚠️ — listing/aggregation degrade with volume.

11. Logging

  • Winston standard; wallet debits in wallet_transactions; recovery sends logged in auto_campaign_roi.
  • Review crons update *_last_reviewed markers 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 → lowRatingTrigger recovery.
  • Which review sources exist today? Google Play + Apple + internal order-level; GMB planned 📄.
  • Is /nps/data authenticated? 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)

Knowledge Tests

Level 1 — MCQs

  1. A feedback-form rating is stored with source = a) "1" b) "2" c) "3" d) "4" — c.
  2. NPS Detractor range is a) 0–6 b) 7–8 c) 9–10 d) 1–3 — a 📄.
  3. The low-rating journey re-triggers only while a) wallet>0 b) rating_frequency < frequency_rating c) rating>4 d) always — b.
  4. Apple reviews are stored in a) google_reviews b) ratings_reviews c) ios_reviews d) nps_aggreator — c.
  5. npsData cron currently runs for a) all parents b) parent 7175 only c) child businesses d) none — b ⚠️.

Level 2 — Scenarios

  1. A brand complains customers who gave 2★ never got the recovery offer. Walk through the three gate conditions in lowRatingTrigger you'd check, in order. (Expect: journey config exists/enabled → rating ≤ threshold → rating_frequency < frequency_rating; plus wallet balance.)
  2. NPS dashboard shows all Detractors as "6" for a brand that uses a 1–5 star form. Explain how the answer*2 conversion 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

  1. Write a read-only audit that verifies, for a sample of low ratings yesterday, whether a recovery send was enqueued and rating_frequency incremented (directly probing E1.7).
  2. Propose the minimal index set for ratings_reviews (parent_business_id, source, inserted_at, mobile_number) with justification per query in §6.
  3. Draft the design (no code) to add GMB as a third review-aggregation cron mirroring googleReviews.js, including collection shape and dedup key.