Skip to content

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-crm backend (confirmed against map-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 in salonController. 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]
  1. Admin designs a plan (price, validity, benefits, stacking rules, purchase channels) in the plan builder.
  2. Plan sits as Draft; publishing makes it Active and buyable.
  3. Guest purchases via App/Web/POS. An optional activation delay may apply.
  4. At checkout, POS auto-applies the eligible benefit; the discounted amount is captured on the order as membership_used and flows through ingestion into order/analytics.
  5. 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_used on 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_used on orders; plan-shaped config presumed in wallet_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 memberships table/collection was found in map-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 in routes/crm.js and controllers/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_orders with 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 in salonController log calls). Grep logs for these tags when a report is wrong.
  • Ingestion logs membership_used parsing 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.



Knowledge Tests

Level 1 β€” MCQs

  1. Where is the membership plan-builder implemented in uengage-crm? a) controllers/membershipController.js b) controllers/loyaltyController.js c) It is not in this backend d) routes/membership.js Answer: c. No membership controller/route exists in the repo; the builder is frontend/external (map-02 confirms "No dedicated membership controller").

  2. 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 in membershipReport.

  3. Which endpoint actually exists in this backend? a) POST /membership/create b) GET /get/membership/details c) POST /membership/purchase d) PUT /membership/pause Answer: b. Registered in routes/crm.js β†’ salonController.membershipReport. The others are πŸ“„ Excel-only.

  4. 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).

  5. Do members still earn loyalty points on orders? a) No b) Yes, via crm_points_allocation_queue c) 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

  1. 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_used consumption. Route them to the frontend team; confirm ingestion is capturing membership_used.

  2. Membership revenue in membershipReport suddenly drops to β‚Ή0 for one outlet. Diagnose. Expected: Check whether the POS/frontend stopped sending membership_used on that outlet's orders; verify injestionController/processCommonIngestion still parse it; confirm date range and businessId. 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

  1. Prove the gap. Run grep -ril membership controllers/ routes/ prism_routes/ and grep -n membership_used commonFunctions/processCommonIngestion.js controllers/injestionController.js. Write a 5-line note listing every backend touchpoint of membership and label each βœ…/πŸ“„.
  2. Trace a report. Follow /get/membership/details from routes/crm.js into salonController.membershipReport; document the exact MongoDB/MySQL query, the aggregation, and the response shape. Correct any ⚠️/πŸ“„ markers on this page with your findings.
  3. Design the missing module. Draft (do not build) a memberships table 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).