Digital-Bills Module¶
One-line: After an order, Prism sends the customer a branded web receipt (via WhatsApp or SMS, immediately or delayed 15–240 min) containing the itemized bill + loyalty info + upsell/banner widgets + feedback link + profile-completion widget — and tracks the full engagement funnel (bills sent → clicks → unique clicks → opening %) in
digital_bill_mis.
Validation legend¶
| Symbol | Meaning |
|---|---|
| ✅ | Verified in code this review |
| 📄 | From PM Excel / business docs |
| 💻 | Code detail confirmed |
| ♻️ | Duplicated / overlapping implementation |
| ⚰️ | Legacy / temporary |
| ⚠️ | Bug, risk, or footgun |
1. Overview¶
Digital Bills is a paid sub-product (📄 per-bill delivery charge, revenue model in Product-Overview §2). It replaces the paper receipt with a branded, interactive microsite. Configuration is a setup wizard persisted in MySQL link_config + link_widget_mapping; delivery + tracking run through MongoDB queues and MIS.
Two config surfaces:
- Bill config ✅ — mode (1=SMS, 2=WhatsApp), templates, sender ID, brand color, footer, linkValidity, delivery delay time, and bill_platform (which order sources trigger a bill).
- Widgets ✅ — ordered (sequence) list of widget blocks rendered inside the bill (itemized receipt, loyalty, upsell, feedback link, profile-completion, static banner).
⚠️ Known bug E1.8 — "Digital bills report logic" (P1) 📄 (Roadmap). The reporting funnel (
digital_bill_mis) and the dailydigitalBillMailreport are flagged for correctness review — notably the derived opening % and how enabled-but-unsent businesses are attributed.
2. Business Flow¶
Example journey¶
- Setup (one-time) 📄 — Admin enables Digital Bills, picks channel/template/sender, sets delivery delay and platforms, arranges widgets. Go-Live checklist step 5 (Business-Flow §1).
- Order completes → order lands in
crm_queue. processDigitalBills(or the order pipeline) matches business config, builds the bill payload, creates a short link (https://uen.io/{code}), enqueues SMS/WhatsApp withscheduled_at = now + time.- Wallet is debited for the send (SMS or WhatsApp cost + GST).
- Customer opens the branded bill →
invoiceClickfires → funnel counters increment. - Customer interacts with widgets (
widgetInsight), taps feedback link (feedbackClick), downloads PDF (invoiceDownload).
Edge cases ✅¶
- No config — child config missing → falls back to parent (
parentId=0); if neither, bill is skipped. - Masked / invalid mobile — Indian 10-digit
[6-9]only; sequential/repeated numbers rejected → no bill. - Duplicate order — existing
order_idindigital_bill_customersfor that child → skipped (idempotency). - Insufficient wallet — send blocked (surfaced in
digitalBillMailas reason "wallet balance < 0.30"). - Platform gate — order source not in
bill_platformarray → skipped (null/empty = all platforms). - Special parents — parent
27234skips "Pick Up" orders ⚠️ (hardcoded).
3. Technical Flow¶
- Source ✅ — POS/order events in
crm_queue(source=2petpooja handled by cron; other sources via order pipeline). Config comes from the wizard APIs. - API ✅ — Config routes (
/links/*, auth) + insight routes (/invoice/*,injestion_middleware). - Validation ✅ — mobile format; platform gate; duplicate
order_id; wallet balance. - Logic ✅ —
commonFunctions/digitalBill.jsbuilds the bill (customer, order, items, loyalty points, feedback-form JWT), computesscheduled_at, chooses SMS vs WhatsApp, renders template variables. - DB ✅ — writes
digital_bill_customers,digital_bill_mis,wallet_transactions; readslink_config,link_widget_mapping,business_configs,wallet_config,feedback_configs. - Events / Queue ✅ — inserts into
sms_queue_YYYYMMDD(mode 1) orwhatsapp_queue_YYYYMMDD(mode 2). Drained by the SMS/WhatsApp send crons (Queues). - Notification ✅ — customer receives the link; opens/clicks tracked by
invoiceInsightsController. - Response ✅ — link created via
POST https://api.uengage.in/links/createLink→{shortened_url, bill_id}.
4. Architecture Diagram¶
Flowchart¶
flowchart TD
subgraph Config["Setup wizard (MySQL)"]
LC[(link_config)]
LWM[(link_widget_mapping)]
LWC[(link_widget_copy)]
end
DBC[digitalBillController] --> LC
DBC --> LWM
DBC --> LWC
subgraph Gen["Bill generation"]
Q[(crm_queue source=2)]
CRON[processDigitalBills 09:57 IST]
FN[commonFunctions/digitalBill.js]
LINK[(api.uengage.in/links/createLink)]
end
Q --> CRON --> FN
FN --> LINK
FN -->|mode 1| SQ[(sms_queue_YYYYMMDD)]
FN -->|mode 2| WQ[(whatsapp_queue_YYYYMMDD)]
FN --> DBCUST[(digital_bill_customers)]
FN --> DBMIS[(digital_bill_mis)]
FN --> WT[(wallet_transactions)]
subgraph Track["Engagement tracking"]
II[invoiceInsightsController]
end
CUSTOMER((Customer)) -->|open| II
CUSTOMER -->|widget tap| II
CUSTOMER -->|feedback| II
CUSTOMER -->|download| II
II --> DBCUST
II --> DBMIS
II --> FBMIS[(feedback_mis)]
subgraph Report["Daily report"]
MAIL[digitalBillMail 11:32 IST]
end
DBMIS --> MAIL --> EMAIL[[Ops email + XLSX]]
Sequence — generate + track¶
sequenceDiagram
autonumber
participant CRON as processDigitalBills
participant CFG as link_config / business_configs
participant FN as digitalBill.js
participant LINK as links/createLink
participant Q as sms_queue / whatsapp_queue
participant CUST as Customer
participant II as invoiceInsightsController
participant MIS as digital_bill_mis
CRON->>CFG: match config (child→parent fallback)
CRON->>FN: digitalBill(order)
FN->>FN: validate mobile, platform gate, dedup order_id
FN->>LINK: POST create link
LINK-->>FN: {shortened_url, bill_id}
FN->>Q: enqueue (scheduled_at = now + time)
FN->>MIS: inc total_bill_sent, total_sms/whatsapp_sent
Note over Q: SMS/WhatsApp cron delivers link
CUST->>II: POST /invoice/open {bill_id}
II->>MIS: inc total_clicks; inc unique_clicks if first click
5. Folder Structure (real files)¶
uengage-crm/
├── controllers/
│ ├── digitalBillController.js ✅ config wizard + customer/MIS reads
│ └── invoiceInsightsController.js ✅ click / widget / feedback / download tracking
├── commonFunctions/
│ ├── digitalBill.js ✅ bill builder + delivery + wallet debit
│ └── digitalBillTemp.js ⚰️ temp/test variant (manual_insert_temp=1)
└── crons/
├── processDigitalBills.js ✅ crm_queue → bill generation (09:57 IST)
└── digitalBillMail.js ✅ daily Ops report (11:32 IST)
6. Database¶
Config (MySQL)¶
| Table | Purpose | Key fields ✅ |
|---|---|---|
link_config |
Per-business bill config | businessId, parentId (0=parent), mode (1 SMS / 2 WA), type, linkValidity, brandColor, enableFooter, footerButton, time (delay min), status, sms_template, whatsapp_template, sender_id, sms_variables, whatsapp_variables, bill_platform (app/website/kiosk/pos/waba), insertedAt, updatedAt |
link_widget_copy |
Master list of available widgets | id, name, status |
link_widget_mapping |
Per-business ordered widgets | businessId, parentId, widgetId, sequence, title, value, static_banner_link, status, insertedAt |
Tracking (MongoDB)¶
| Collection | Purpose | Key fields ✅ |
|---|---|---|
digital_bill_customers |
One row per bill sent | bill_id, name, number, order_id, sent_at, mode, short_url, clicked, download_flag, sent_count, delivered, read_count, message_error, widgets[] (widgetId,clicks,watchTime), roi_calculated, order_source, parent_business_id, child_business_id |
digital_bill_mis |
Daily funnel per parent/child | date, total_bill_sent, total_clicks, unique_clicks, total_sms_sent, total_whatsapp_sent, parent_business_id, child_business_id, created_at, updated_at |
wallet_config |
Pricing | digital_bill_sms (₹0.145), digital_whatsapp_cost (₹0.92), digital_whatsapp_cost_utility (₹0.25), sms_gst/whatsapp_gst (18) |
feedback_configs |
Feedback flow gate | per parent/child (child_business_id="0" = parent) |
feedback_customers / feedback_mis |
Feedback link funnel | mirrors bill funnel |
Indexes: none defined on the MIS/customer collections ⚠️ (see map-03 §5.1); the
digital_bill_customersdedup lookup bychild_business_id+order_idis unindexed.
Relationships¶
link_config (MySQL, by businessId/parentId) drives generation → each send writes one digital_bill_customers doc keyed by order_id and a daily digital_bill_mis roll-up. The feedback link inside a bill flows into the Feedback-NPS funnel (feedback_customers/feedback_mis).
The funnel (E1.8 relevant) ✅¶
| Metric | Field | Increment rule |
|---|---|---|
| Bills sent | total_bill_sent |
+1 per generated bill |
| Total clicks | total_clicks |
+1 on every /invoice/open |
| Unique clicks | unique_clicks |
+1 only when clicked==1 (first open) |
| Opening % | derived | unique_clicks / total_bill_sent |
Common queries ✅¶
// funnel for a date range
db.digital_bill_mis.aggregate([
{ $match: { parent_business_id, date: { $gte, $lte } } },
{ $group: { _id:null, sent:{$sum:"$total_bill_sent"}, clicks:{$sum:"$total_clicks"}, uniq:{$sum:"$unique_clicks"} } }
])
// dedup check before generating
db.digital_bill_customers.findOne({ child_business_id, order_id })
7. APIs¶
Config (auth: authMiddleware, all POST)¶
| Route | Purpose | Function |
|---|---|---|
/links/getWidgetList |
List available widgets | listing_widgets |
/links/addBusinessConfig |
Create/update bill config | addBusinessConfig |
/update/bill/status |
Toggle bill on/off (MySQL + business_configs.digital_bill) |
updateBillStatus |
/links/getBusinessConfig |
Read config + widgets | getBusinessConfig |
/links/addWidget |
Add widget to bill | addWidget |
/links/editWidget |
Edit widget | editWidget |
/get_digital_bill_customers |
Paginated sent list (10/page) | getDigitalBillCustomers |
/get_digital_bill_mis |
Funnel aggregates | getDigitalBillMis |
Insights (auth: injestion_middleware, all POST) ✅¶
| Route | Records | Function |
|---|---|---|
/invoice/open |
clicked++, funnel total_clicks/unique_clicks |
invoiceClick |
/feedback_invoice/open |
feedback clicked++, feedback_mis |
feedbackClick |
/update/widget/insight |
widgets.$[w].clicks++ or .watchTime set |
widgetInsight |
/invoice/download |
download_flag++ |
invoiceDownload |
Representative example — record open (verify)¶
Validation ✅:bill_id required. Failures: 400 missing bill_id; unknown bill_id → no-op update.
Representative example — add config (verify)¶
// POST /links/addBusinessConfig Headers: { token: <auth> }
{ "business_id":"12345","parent_business_id":"999","mode":2,"type":1,
"linkValidity":30,"brandColor":"#C0392B","enableFooter":1,"footerButton":"Order Again",
"time":30,"status":1,"whatsapp_template":"digital_bill","sender_id":"BRAND",
"platform":["pos","app"] }
// 200 → { "status": 1, "message": "config saved" }
time is the delivery delay in minutes (immediate = 0; business range 15–240 📄).
8. Code Walkthrough¶
Generation ✅
processDigitalBills (cron 09:57 IST)
└─ poll crm_queue {status:1, source:2} (batch 10,000)
└─ resolve business (crm_token OR petPooja_business_mapping+addo_business)
└─ skip Uengage/Zomato/Swiggy sources; skip dup order_id; parent 27234 skips Pick Up
└─ digitalBilFlow / digitalBill(businessData, data, "petpooja", "pos", points)
├─ validate mobile [6-9]\d{9}, platform gate (mapPlatform)
├─ scheduled_at = moment().add(link_config.time,"minutes")
├─ POST api.uengage.in/links/createLink → {shortened_url, bill_id}
├─ build bill: customer, order (subTotal/tax/discount/loyaltyPoints), items[], feedback JWT
├─ mode 1 → sms_queue_* · mode 2 → whatsapp_queue_* (gupshup|facebook)
├─ debit wallet (cost + GST) → wallet_transactions
└─ insert digital_bill_customers + upsert digital_bill_mis
└─ set crm_queue.status = 3
Tracking ✅ — invoiceInsightsController:
- invoiceClick(bill_id) — $inc clicked; if clicked==1 also $inc unique_clicks; always $inc total_clicks (+upsert digital_bill_mis by date).
- widgetInsight(bill_id, widget_id, event_type, watch_time) — arrayFilters update widgets.$[w].clicks ($inc) or .watchTime ($set).
- feedbackClick, invoiceDownload — analogous counters.
Reporting ✅ — digitalBillMail (11:32 IST) aggregates digital_bill_mis, compares yesterday vs last-week/2-weeks-ago via dashboard_mis (total_pos_count,total_uen_count), lists enabled-but-unsent businesses with reasons, emails an XLSX to Ops via POST https://www.uengage.in/addoapi/sendCRMMail.
Dependencies: axios (link creation + mail), moment-timezone (IST scheduling), MySQL pool (link_* tables), SMS/WhatsApp queues (downstream send).
⚰️
digitalBillTemp.jsis a temp/test twin (hardcoded queue date,manual_insert_temp=1) —processDigitalBillscurrently imports it asdigitalBilFlow⚠️ (verify which builder is authoritative in prod).
9. Business Rules¶
| Rule | Value ✅ | Source |
|---|---|---|
| Delivery delay | link_config.time minutes (immediate=0; 15–240 range 📄) |
digitalBill.js |
| Channel | mode 1=SMS, 2=WhatsApp |
link_config |
| Platform gate | bill_platform ∈ {app, website, kiosk, pos, waba}; null=all |
mapPlatform |
| Config fallback | child → parent (parentId=0) |
✅ |
| Dedup | one bill per (child_business_id,order_id) |
✅ |
| SMS cost | ₹0.145 + 18% GST | wallet_config |
| WhatsApp cost | ₹0.92 marketing / ₹0.25 utility + 18% GST | wallet_config |
| Cron generate | 09:57 IST, batch 10,000 | processDigitalBills |
| Report | 11:32 IST daily → Ops XLSX | digitalBillMail |
| Mobile validity | [6-9]\d{9}, reject sequential/repeated |
✅ |
| Unsent reason threshold | wallet balance < 0.30 | digitalBillMail |
Hidden logic ⚠️: parent 27234 Pick-Up exclusion; digitalBillTemp temp builder in the cron path; feedback-form JWT hash secret embedded in digitalBill.js (security-review item, Security).
Widgets in the bill 📄/✅: itemized receipt, loyalty info (points earned), upsell/product widgets, static banner, feedback-form link, profile-completion widget — configured/ordered via link_widget_mapping.sequence.
10. Performance¶
processDigitalBillsbatches 10,000 with 0 ms inter-doc delay — CPU-bound on parse/HTTP; the per-ordercreateLinkHTTP call is the latency floor ⚠️.- Dedup and MIS lookups are unindexed ⚠️ — will degrade as
digital_bill_customersgrows (date-partitioned queues help downstream, not this collection).
11. Logging¶
- Winston standard; per-order failures logged and skipped (cron continues).
- Wallet debits recorded in
wallet_transactions(auditable ledger).
12. Monitoring¶
digitalBillMailis the de-facto daily monitor: sent counts, week-over-week deltas, enabled-but-unsent + reason.- Watch opening % drift for E1.8; reconcile
total_bill_sentvs actualdigital_bill_customersinserts.
13. Troubleshooting¶
| Issue | Cause | Fix | Command |
|---|---|---|---|
| Bills enabled but none sent | Wallet < 0.30, no orders, masked numbers, config error | Read the reason in daily report | inspect digitalBillMail output |
| Opening % looks wrong (E1.8) | unique_clicks/total_bill_sent mismatch |
Recompute from raw docs | db.digital_bill_customers.count({child_business_id, sent_at:{$gte}}) vs digital_bill_mis.total_bill_sent |
| Duplicate bill sent | dedup lookup missed (unindexed / race) | Add index; check order_id | db.digital_bill_customers.find({order_id}).count() |
| Bill not sent for a source | source not in bill_platform |
Add platform to config | check link_config.bill_platform |
| Wrong delivery time | time misconfigured |
Update config | SELECT time FROM link_config WHERE businessId=? |
14. FAQs¶
- Immediate vs delayed?
time=0immediate; elsenow + timeminutes (15–240 typical). - SMS or WhatsApp?
modeonlink_config(1/2). - Where's the feedback link? Injected into the bill as a JWT-signed URL → routes into Feedback-NPS.
- Which builder runs in the cron?
digitalBillTemp(imported asdigitalBilFlow) — verify vsdigitalBill.js⚠️.
15. Cheat Sheet¶
config : MySQL link_config (mode 1=SMS/2=WA, time=delay min, bill_platform[])
widgets : MySQL link_widget_mapping (sequence order)
generate : processDigitalBills 09:57 IST, crm_queue source=2, batch 10k
funnel : digital_bill_mis {total_bill_sent, total_clicks, unique_clicks}
opening % : unique_clicks / total_bill_sent (E1.8 under review)
track APIs : /invoice/open, /update/widget/insight, /feedback_invoice/open, /invoice/download
cost : SMS ₹0.145 · WA ₹0.92 (util ₹0.25) + 18% GST
report : digitalBillMail 11:32 IST → Ops XLSX
KNOWN BUG : E1.8 digital bills report logic (P1)
Related Modules¶
- WhatsApp & SMS-Push — the delivery channels (
whatsapp_queue_*,sms_queue_*). - Feedback-NPS — the feedback link inside every bill.
- Campaigns / Automated-Journeys — share the queue + wallet infrastructure.
- Queues · Database · Third-Party · Roadmap E1.8
Knowledge Tests¶
Level 1 — MCQs¶
link_config.mode = 2means? a) SMS b) WhatsApp c) email d) disabled — b.unique_clicksincrements when? a) every open b) only the first open of a bill c) on download d) on widget tap — b (clicked==1).- Opening % is derived as? a) clicks/sent b) unique_clicks/total_bill_sent c) download/sent d) sent/clicks — b.
- A bill is skipped if the order_id already exists in? a) crm_queue b) digital_bill_mis c) digital_bill_customers d) link_config — c.
time=30in link_config controls? a) validity days b) delivery delay minutes c) GST d) widget count — b.
Level 2 — Scenarios¶
- A brand says "we enabled Digital Bills yesterday but zero went out." Which report tells you why, and name three possible reasons it lists. (Expect:
digitalBillMail; wallet < 0.30, no orders, masked numbers, config error.) - Opening % on the dashboard is 140%. Explain how E1.8 / the increment rules could produce >100% and which two fields you'd reconcile. (Expect:
total_clicksvsunique_clicksconfusion, ortotal_bill_sentundercount; reconcile against rawdigital_bill_customers.)
Level 3 — Code reading¶
In invoiceInsightsController.invoiceClick, trace what happens to digital_bill_mis on the second open of the same bill. (Answer: total_clicks +1, unique_clicks unchanged because clicked is now 2, not 1.)
Level 4 — Architecture¶
processDigitalBills calls createLink over HTTP per order inside a 10,000 batch. Propose an architecture change to remove this as the throughput bottleneck and state the trade-off for E1.8 reporting accuracy.
Level 5 — Prod debugging¶
Duplicate bills are reaching a subset of customers. Give the query to confirm duplication and the most likely root cause given the dedup lookup is unindexed and the cron batches concurrently.
Practical Assignments¶
- Write a read-only reconciliation script comparing
digital_bill_mis.total_bill_sentto counteddigital_bill_customersfor one parent/day (directly probing E1.8). - Propose and justify indexes for
digital_bill_customers(dedup) anddigital_bill_mis(funnel range) in a short note. - Document the exact widget set rendered in a bill by reading
link_widget_copy+ a samplelink_widget_mapping, mapping eachwidgetIdto its business purpose.