Membership Module β Paid Loyalty Plans¶
Validation legend: β verified in code Β· π Excel/spec-only (not found in backend) Β· π» code-only (no Excel) Β· β»οΈ duplicate Β· β°οΈ dead code Β· β οΈ risk/known-issue
[!WARNING] This is the most important thing to know about this module. The PM Excel describes a rich, standalone paid-membership product (8-tab plan builder, DraftβActiveβPausedβExpired lifecycle, benefits engine, POS auto-apply, App/Web/POS purchase). There is NO dedicated membership controller, route file, or plan-builder API in the
uengage-crmbackend (confirmed againstmap-02-controllers.mdβ the Membership row reads "No dedicated membership controller found"). What does exist in the backend is: (a) membership consumption fields carried through order ingestion (membership_used), and (b) membership reporting endpoints insalonController. The plan-builder and purchase UI are almost certainly implemented in the frontend (Next.js) and/or a service outside this repo. See Β§ Validation Gap before you trust any endpoint on this page.
1. Overview¶
Membership is Prism's paid loyalty tier: a customer pays upfront for a plan and receives benefits over a validity window. Where the free Loyalty program earns value from behavior, membership sells value in advance. It is a headline monetization lever for the restaurant client and a retention lock-in for the guest.
Product truth (π Excel): - One active membership per customer. Non-refundable, non-transferable. - Benefits: % discount, flat discount, free item(s), free delivery, special/hidden menu, slash price (member-only item price), experiential perks. - Plans are built with an 8-tab plan builder and move through a strict lifecycle (Draft β Active β Paused β Expired). Once Active, price/benefits/validity are locked β you must Pause β Duplicate β edit. - Purchase channels: App, Web, POS. Benefits auto-apply at POS checkout. - CRM notifications fire on purchase, redemption, and expiry.
Backend truth (β
code): membership shows up as (1) an amount used on an order (membership_used, ingested per order) and (2) salon/F&B reports (salonController.membershipReport, membershipPercentageReport, membershipUsage, redemption check). Plan definition config is co-located with loyalty config in the MySQL wallet_rules table (same table that holds redemptionPct, loyaltyDays, milestones).
2. Business Flow¶
2.1 Example journey (π Excel)¶
flowchart LR
A[Admin builds plan<br/>8-tab builder] --> B[Plan = Draft]
B --> C[Publish β Active]
C --> D[Customer buys<br/>App / Web / POS]
D --> E[Benefits activate<br/>optional delay]
E --> F[Auto-apply at POS checkout<br/>membership_used recorded on order]
F --> G[CRM notifications:<br/>purchase / redemption / expiry]
G --> H[Reports:<br/>membershipReport, redemption rate, renewal rate]
- Admin designs a plan (price, validity, benefits, stacking rules, purchase channels) in the plan builder.
- Plan sits as Draft; publishing makes it Active and buyable.
- Guest purchases via App/Web/POS. An optional activation delay may apply.
- At checkout, POS auto-applies the eligible benefit; the discounted amount is captured on the order as
membership_usedand flows through ingestion into order/analytics. - Prism sends CRM notifications and rolls the usage into membership reports.
2.2 Edge cases (π Excel + β where noted)¶
| Case | Rule | Validation |
|---|---|---|
| Buying a 2nd membership while one is active | Blocked β one active membership per customer | π |
| Renewal β same plan | Extends validity from current expiry | π |
| Renewal β different plan while active | Blocked until current expires | π |
| Editing an Active plan | Locked; must Pause β Duplicate β edit (protects members who already bought the old terms) | π |
| Stacking with loyalty/promo | Defaults: loyalty earn ON, loyalty burn OFF, promo OFF (configurable) | π (Terminologies) |
| Order carries membership discount | membership_used field is captured and summed alongside wallet_used + discount in reporting |
β
(salonController, injestionController) |
| Refund / cancel | Membership non-refundable; benefit consumption already recorded | π |
3. Technical Flow¶
Because there is no membership controller, the realized backend flow is consumption + reporting only:
Source (POS/App)
β order payload carries membership_used (discount attributable to membership)
β injestionController / processCommonIngestion parses parseData.membership_used
β stored on the order record (Mongo orders + MySQL addo_orders)
β salonController.membershipReport / membershipPercentageReport / membershipUsage
aggregate membership_used across orders (Xlsx export)
β check/membership/redemption verifies a redemption
- Source β API: membership discount is computed by the POS/frontend, not by this backend. The backend only receives
membership_usedon the ingested order. - Validation / logic: none of the plan-lifecycle logic (Draft/Active/Paused/Expired, one-active-per-customer, stacking) is enforced in this repo β it lives upstream (frontend/POS).
- DB:
membership_usedon orders; plan-shaped config presumed inwallet_rules(shared with loyalty). See Β§6. - Events / queues / notifications: loyalty earning on a membership order still flows through
crm_points_allocation_queue(see Loyalty); membership-specific notifications are π Excel-only in this repo. - Response: reporting endpoints return aggregated JSON and/or an Xlsx file URL.
β οΈ Do not document membership purchase/plan-CRUD endpoints here β they do not exist in this repo. If you find them later, add them under Β§7 with a β .
4. Architecture Diagram¶
4.1 What the spec says vs what the backend does¶
flowchart TB
subgraph spec["π Excel spec (product)"]
PB[8-tab Plan Builder]
LC[Lifecycle engine<br/>DraftβActiveβPausedβExpired]
PUR[Purchase App/Web/POS]
BEN[Benefits + auto-apply]
NOTI[Purchase/Redeem/Expiry notifications]
end
subgraph repo["β
uengage-crm backend (reality)"]
ING[injestionController<br/>reads membership_used]
ORD[(orders / addo_orders)]
RPT[salonController<br/>membershipReport / usage / redemption]
WR[(MySQL wallet_rules<br/>loyalty+membership config)]
end
PB -.->|likely Next.js / external| WR
LC -.->|not in repo| repo
PUR -.->|not in repo| ING
BEN --> ING --> ORD --> RPT
WR --> RPT
4.2 Plan lifecycle state machine (π Excel)¶
stateDiagram-v2
[*] --> Draft: admin creates
Draft --> Active: publish (price/benefits/validity LOCK)
Active --> Paused: admin pauses (stops new sales)
Paused --> Active: resume
Active --> Expired: validity ends
Paused --> Expired: validity ends
Paused --> Draft: duplicate β edit (new plan)
Expired --> [*]
5. Folder Structure (real files)¶
There is no controllers/membershipController.js. The realized surface area:
uengage-crm/
βββ controllers/
β βββ salonController.js β
membershipReport, membershipPercentageReport,
β β membershipUsage, check/membership/redemption
β βββ injestionController.js β
reads parseData.membership_used on ingest
βββ commonFunctions/
β βββ processCommonIngestion.js β
carries membership_used through ingestion
βββ routes/
β βββ crm.js β
/get/membership/details, /percentage/details,
β /usage, /check/membership/redemption
βββ (plan builder / purchase) π NOT in this repo β frontend (Next.js) / external
6. Database¶
[!NOTE] No dedicated
membershipstable/collection was found inmap-03-database.md. Membership data is embedded / denormalized:
| Store | Object | Membership-relevant fields | Purpose | Validation |
|---|---|---|---|---|
| MySQL | wallet_rules |
shared loyalty+membership config row per parentBusinessId (redemptionPct, loyaltyDays, milestones, walletName, β¦). Membership plan definitions are presumed to live here or in a frontend store. |
Program configuration | β οΈ inferred |
| MySQL | addo_orders |
order-level membership discount | Revenue/benefit attribution | β |
| MongoDB | orders |
membership_used |
Per-order membership consumption | β |
| MySQL | addo_contacts_mapping |
customerβbusiness link; would hold "active membership" if enforced | Customer membership state | π (not confirmed) |
Relationships: customer (addo_contacts β addo_contacts_mapping.contactMappingId) β orders (membership_used). Same customer key (10-digit mobileNo) as everywhere else in Prism.
Indexes: none membership-specific. Reports scan orders by businessId + date range β see the Database indexing-gap notes (missing (businessId, orderDate) index).
Common query (β
pattern, from salonController): sum membership consumption per order β
// membershipReport aggregation (simplified from salonController.js)
{ $sum: ["$membership_used", "$wallet_used", "$discount"] } // "Total Benefit"
7. APIs¶
[!IMPORTANT] Every endpoint below is reporting/consumption, registered in
routes/crm.jsβsalonController. There is no create/purchase/plan-lifecycle endpoint in this backend. Treat these as representative β verify inroutes/crm.jsandcontrollers/salonController.js.
| Method | Path | Handler | Purpose | Validation |
|---|---|---|---|---|
| POST/GET | /get/membership/details |
salonController.membershipReport |
Membership usage report (Xlsx/JSON) | β route confirmed |
| POST/GET | /get/membership/percentage/details |
salonController.membershipPercentageReport |
Membership as % of revenue | β route confirmed |
| POST/GET | /get/membership/usage |
salonController.membershipUsage |
Usage breakdown | β route confirmed |
| POST | /check/membership/redemption |
salonController (redemption check) |
Verify a membership redemption | β route confirmed |
Headers / auth: these ride on the standard CRM auth middleware used across routes/crm.js (token-based). Verify the exact middleware in routes/crm.js β do not assume.
Representative request/response (verify in code):
// POST /get/membership/details
// request
{ "parentBusinessId": 123, "businessId": 456, "startDate": "2026-06-01", "endDate": "2026-06-30" }
// response (shape verify in salonController.membershipReport)
{ "status": true, "data": [ { "Membership Used": 250, /* ...order rows... */ } ] }
Failures: missing date range / business id β validation error; empty range β empty data. Exact error envelope: verify in code.
Plan-builder / purchase APIs: π Not in this repo. If a caller asks for them, point to the frontend/Next.js codebase or the POS integration owner.
8. Code Walkthrough¶
routes/crm.js
ββ /get/membership/* β salonController.membershipReport / membershipPercentageReport / membershipUsage
ββ query MongoDB orders (+ MySQL addo_orders) filtered by business + date
ββ aggregate membership_used (Β± wallet_used, discount)
ββ build Xlsx (xlsx lib) β upload β return URL [pattern shared with other salon reports]
injestionController.js / processCommonIngestion.js
ββ parseData.membership_used β persisted on order record
(this is the ONLY place membership data enters the backend)
Dependencies: xlsx (report export), MongoDB orders, MySQL addo_orders, S3 (report upload). No dependency on any membership-specific engine β because none exists here.
9. Business Rules¶
| Rule | Value | Source | Validation |
|---|---|---|---|
| One active membership per customer | enforced upstream (not in this repo) | Excel | π |
| Active plan is immutable (price/benefits/validity locked) | PauseβDuplicateβedit | Excel | π |
| Renewal: same plan extends; different plan blocked while active | β | Excel/Business-Flow | π |
| Non-refundable, non-transferable | β | Excel | π |
| Stacking defaults | loyalty earn ON, burn OFF, promo OFF | Terminologies | π |
| Benefit types | %/flat discount, free item, free delivery, special menu, slash price, experiential | Excel | π |
| Loyalty earning on membership orders | still flows via crm_points_allocation_queue |
code | β |
membership_used recorded on every discounted order |
β | ingestion | β |
Feature flags / permissions: plan CRUD is an Admin role action (π); reporting is Analyst/Manager. Enforcement of role gating for the report endpoints: verify middleware in routes/crm.js.
Hidden logic (β οΈ): because membership shares the wallet_rules row with loyalty, changing loyalty config could unintentionally touch membership-adjacent config. Treat wallet_rules edits carefully.
10. Performance¶
- Report endpoints do date-range scans over
orders/addo_orderswith no membership index β heavy for large brands over long windows. Mitigation: narrow date ranges, run off-peak, add(businessId, orderDate)index (Database Β§5.1). - Xlsx generation is synchronous in-request β large exports can block the web worker. β οΈ Prefer async/job export for big brands (pattern used by
customerInsightsController).
11. Logging¶
- Report handlers log through the shared Winston logger (tag
"membershipReport"/"membershipPercentageReport"appears insalonControllerlog calls). Grep logs for these tags when a report is wrong. - Ingestion logs
membership_usedparsing under the ingestion pipeline logs.
12. Monitoring¶
- No membership-specific dashboards/crons. Monitor via: order ingestion health (membership_used arriving), and the report endpoints' error logs.
- β οΈ Because plan lifecycle is not in this backend, backend metrics cannot tell you about active/paused/expired plan counts β that lives in the frontend/POS system.
13. Troubleshooting¶
| Issue | Likely cause | Fix | Commands |
|---|---|---|---|
| Membership report shows βΉ0 | membership_used not sent by POS/frontend, or not parsed |
Check ingested order payload for membership_used; verify injestionController parse path |
grep -n "membership_used" controllers/injestionController.js commonFunctions/processCommonIngestion.js |
| "Where is the create-plan API?" | It's not in this repo | Point to frontend (Next.js) / POS owner | grep -ril membership controllers/ routes/ prism_routes/ (returns only salon/injestion) |
| Benefit not auto-applying | POS-side rule, not backend | Escalate to POS integration | n/a in this repo |
| Report scan slow/timeout | missing index, wide date range | Narrow range; add index | see Database Β§5.1 |
| Loyalty points wrong on a membership order | This is a loyalty bug (dual-store), not membership | See Loyalty Β§Troubleshooting & bug E1.10 | β |
14. FAQs¶
Q: Does the backend enforce "one active membership per customer"? A: No (π). That rule is enforced upstream (frontend/POS). This repo only records consumption and reports it.
Q: Where are membership plans stored?
A: No dedicated table found. Config is co-located in MySQL wallet_rules (shared with loyalty); plan definitions may live in the frontend store. β οΈ Verify before relying on it.
Q: Do members still earn loyalty points?
A: Yes β order-based earning goes through the normal crm_points_allocation_queue flow (β
). Stacking defaults: earn ON, burn OFF.
Q: What's membership_used?
A: The rupee value of the membership benefit applied to an order, captured at ingestion and summed in reports.
Q: Is Membership V2 built? A: No β V2 (in-place versioning, menu-level pricing, virtual wallet, loyalty multiplier, grace period, one-tap renewal) is roadmap only (Roadmap Β§C). None of it is in this backend.
15. Cheat Sheet¶
Reality: NO membership controller in uengage-crm.
Entry point: membership_used on ingested orders (only backend data in)
Reports: salonController.membershipReport / membershipPercentageReport / membershipUsage
Routes: routes/crm.js β /get/membership/{details,percentage/details,usage}, /check/membership/redemption
Config table: MySQL wallet_rules (shared with Loyalty)
Lifecycle: DraftβActiveβPausedβExpired (π Excel β enforced upstream)
Rule: 1 active membership / customer; Active plan immutable (PauseβDuplicateβedit)
V2: roadmap only (Roadmap Β§C)
Grep: grep -ril membership controllers/ routes/
Validation Gap¶
[!CAUTION] This section is the reason this doc exists. Read it before acting on anything above.
| Excel/spec claim | Backend reality | Verdict |
|---|---|---|
| Standalone Membership module with 8-tab plan builder | No membership controller/route/model in uengage-crm |
π Excel-only |
| DraftβActiveβPausedβExpired lifecycle engine | No lifecycle code found | π Excel-only (frontend/external) |
| Purchase via App/Web/POS | No purchase endpoint in repo | π Excel-only |
| Benefits auto-apply at POS checkout | Computed by POS; backend only receives membership_used |
β οΈ partial |
| CRM notifications (purchase/redeem/expiry) | Not found in this repo | π Excel-only |
| Membership reporting | salonController.membershipReport et al. |
β verified |
membership_used on orders |
parsed in injestionController / processCommonIngestion |
β verified |
| Plan config storage | co-located in MySQL wallet_rules (shared with loyalty) |
β οΈ inferred |
Conclusion: Membership is partial-in-backend. The product is real and shipped (per PM), but its plan-builder, purchase, lifecycle and enforcement live outside this repository (Next.js frontend and/or POS). This backend contributes consumption capture and reporting only. Do not fabricate backend endpoints; when in doubt, grep -ril membership controllers/ routes/ prism_routes/ and you will find only salonController and injestionController.
Roadmap note: Membership V2 is a 4-phase plan (Roadmap Β§C) β none shipped in this backend yet.
Related Modules¶
- Loyalty β free earning/burning; membership shares
wallet_rulesand the points-allocation queue. - Wallet β points ledger;
wallet_usedsits besidemembership_usedin reports. - Referral β another Loyalty-family incentive.
- Customer-CRM β the customer profile a membership attaches to.
- Database Β· Roadmap Β§C β Membership V2
Knowledge Tests¶
Level 1 β MCQs¶
-
Where is the membership plan-builder implemented in
uengage-crm? a)controllers/membershipController.jsb)controllers/loyaltyController.jsc) It is not in this backend d)routes/membership.jsAnswer: c. No membership controller/route exists in the repo; the builder is frontend/external (map-02 confirms "No dedicated membership controller"). -
What is
membership_used? a) A wallet_rules column b) The rupee value of a membership benefit applied to an order c) A cron d) A Kafka topic Answer: b. It's captured on the ingested order and summed inmembershipReport. -
Which endpoint actually exists in this backend? a)
POST /membership/createb)GET /get/membership/detailsc)POST /membership/purchased)PUT /membership/pauseAnswer: b. Registered inroutes/crm.jsβsalonController.membershipReport. The others are π Excel-only. -
Once a plan is Active, to change its price you mustβ¦ a) Edit it directly b) Delete and recreate c) Pause β Duplicate β edit d) Wait for expiry Answer: c. Active plans are immutable to protect existing members (π Business-Flow edge case).
-
Do members still earn loyalty points on orders? a) No b) Yes, via
crm_points_allocation_queuec) Only if admin toggles per order d) Only on App orders Answer: b. Stacking default is loyalty earn ON; earning uses the normal points queue.
Level 2 β Scenarios¶
-
A client says "our Membership dashboard shows 40 active plans but your API returns nothing for active plans." Explain and respond. Expected: The backend has no plan-lifecycle store; active/paused/expired counts live in the frontend/POS. Backend only reports
membership_usedconsumption. Route them to the frontend team; confirm ingestion is capturingmembership_used. -
Membership revenue in
membershipReportsuddenly drops to βΉ0 for one outlet. Diagnose. Expected: Check whether the POS/frontend stopped sendingmembership_usedon that outlet's orders; verifyinjestionController/processCommonIngestionstill parse it; confirm date range andbusinessId. It's a data-in problem, not a report-logic problem.
Level 3 β Code Reading¶
Open controllers/salonController.js around line 968β1006. The report sums ["$membership_used", "$wallet_used", "$discount"]. Question: if a single order has both a membership benefit AND a loyalty burn, will this sum double-count the customer's total discount, and what report field is affected? Expected: It intentionally aggregates all three discount types into a combined benefit figure; per-order they are distinct amounts so it's additive, not double-counting β but if the POS put the same rupee value in two fields, it would inflate. Verify how the POS populates the fields.
Level 4 β Architecture¶
Prism's High-Level Architecture shows loyalty points spanning Mongo + MySQL non-atomically. Membership has no such engine here. Argue whether membership should be a first-class backend module or stay frontend/POS-driven. Expected: discuss consistency/enforcement (one-active-per-customer, lifecycle) being unsafe to leave to clients, auditability, vs. the current lean footprint; reference the A2.2 "Loyalty & Trust" merge and Membership V2 roadmap as the natural home.
Level 5 β Production Debugging¶
Ticket: "Members are being charged full price at POS for a plan they bought yesterday." You check this backend first. Expected reasoning: Auto-apply is a POS-side rule; this backend neither owns benefits nor enforces active membership. Confirm the order arriving here shows membership_used = 0 (proving POS didn't apply it), then escalate to POS integration β the bug is not in uengage-crm. Show the grep proving no application logic exists here.
Practical Assignments¶
- Prove the gap. Run
grep -ril membership controllers/ routes/ prism_routes/andgrep -n membership_used commonFunctions/processCommonIngestion.js controllers/injestionController.js. Write a 5-line note listing every backend touchpoint of membership and label each β /π. - Trace a report. Follow
/get/membership/detailsfromroutes/crm.jsintosalonController.membershipReport; document the exact MongoDB/MySQL query, the aggregation, and the response shape. Correct any β οΈ/π markers on this page with your findings. - Design the missing module. Draft (do not build) a
membershipstable schema + minimal controller API (create plan, publish, pause, purchase, check-active) that would move enforcement into this backend, honoring the immutability + one-active rules. Cross-reference Membership V2 (Roadmap Β§C).