Membership playbook (Vendor)
Purpose
Section titled “Purpose”This page aligns LionPOS vendor → customer membership with a large-format retail chain mental model: one account, many stores, member savings + loyalty). It complements:
The canonical kind strings and validation live in packages/shared/src/membership/membership-model.ts and apps/backend/src/database/utils/membership-benefits-json.util.ts. The vendor dashboard modal shows the same playbook in a collapsible table.
Plan fee and term (catalog columns)
Section titled “Plan fee and term (catalog columns)”List price and term length live on the product row: membership_products.price_cents and membership_products.duration_days. They are not benefits_json rows. POS validates custom membership lines against price_cents; subscription ends_at defaults use duration_days.
Benefit kind → retail lever → POS today
Section titled “Benefit kind → retail lever → POS today”kind | Value (summary) | Retail member program analogy | Enforced in POS checkout |
|---|---|---|---|
order_discount_percent | 0–100 | Instant basket / “member price” savings on subtotal (after store discounts, before coupon) | Yes |
order_discount_fixed_cents | Minor units (cents) | Fixed member dollars-off | Yes |
loyalty_points_multiplier | > 1.0 | Higher earn rate on rewards | Yes |
loyalty_points_bonus_per_order | Integer ≥ 0 | Bonus points per trip | Yes |
catalog_early_access | Object, string, or null | Early access / gated drops | Yes (Phase A/B/C implemented: visibility + preview pricing + analytics/snapshot) |
free_delivery_threshold_cents | Integer ≥ 0 | Free delivery / shipping threshold (merchandise subtotal in USD cents vs included fee) | Yes (any order_type with included delivery_charge; not when customer pays shipping) |
JSON shape: benefits_json is a JSON array of objects, each { "kind": "<kind>", "value": <typed> }. It is not { "benefits": [ ... ] }.
Rollout checklist (vendor operator + engineering)
Section titled “Rollout checklist (vendor operator + engineering)”Data and migrations
Section titled “Data and migrations”-
membership_products/membership_subscriptionsmigrations applied on the target DB. -
DB_ENABLED=trueand TypeORM repositories available (otherwise vendor APIs returnvendor_requires_database).
Catalog (CRM)
Section titled “Catalog (CRM)”- At least one
vendor_customerplan withstatus = activeand validbenefits_json(or empty). - Store scope intentional: plan
store_idnull = all stores; set = single store only. - Test inactive plan cannot be assigned (API
product_inactive).
Subscriptions
Section titled “Subscriptions”- Customer has
user_idon POS orders when membership savings should apply. - At most one open subscription per vendor + customer (any plan;
customer_open_membership_existsif another isdraft/trialing/active/past_due). - Cancel path sets
canceled/canceled_atand POS stops picking the row.
POS and finance
Section titled “POS and finance”- Place order with logged-in customer: verify
membership_discount_amount/membership_subscription_idinindochina_pos_place_snapshotwhen discount > 0. - Any order type with included
delivery_chargeand threshold: subtotal meets plan →delivery_chargezeroed, snapshotmembership_free_delivery_waivedtrue (when fee is included, not customer-pays-shipping). - Verify expense row / tax allocation per Membership subscriptions — POS.
- Vendor → Customer membership (assign plan on customer profile): collapsible playbook table +
benefits_jsonarray example (sharedVENDOR_CUSTOMER_MEMBERSHIP_BENEFITS_JSON_EXAMPLE), aligned with Membership products modal. - Vendor → Membership products modal: same playbook reference (catalog editor).
QA test matrix (IDs)
Section titled “QA test matrix (IDs)”| ID | Scenario | Steps | Expected |
|---|---|---|---|
| WM-POS-01 | Member percent discount | Active plan with order_discount_percent; customer subscribed; place_order with user_id | Subtotal reduced after store discounts; snapshot contains discount when > 0 |
| WM-POS-02 | Member fixed + percent cap | Plan with both percent and fixed cents; subtotal small | Total membership discount ≤ subtotal after store discounts |
| WM-POS-03 | Loyalty earn multiplier | Plan with loyalty_points_multiplier > 1 | Earned points reflect multiplier (see POS earn path) |
| WM-POS-04 | Guest checkout | Same plan + subscription but no user_id on order | No membership resolution |
| WM-POS-05 | Wrong store | Plan scoped to store A; order at store B | No membership row selected |
| WM-CRM-01 | Second open membership | POST subscription while customer already has active / trialing / draft / past_due on any plan | 409 customer_open_membership_exists |
| WM-CRM-02 | Inactive plan | POST subscription to inactive plan | 409 product_inactive |
| WM-CRM-03 | JSON validation | PATCH plan with invalid benefits_json kind | 400 invalid_benefit_kind or invalid_benefits_json |
| WM-CRM-04 | Newest wins | Two overlapping qualifying subscriptions (avoid in prod) | POS uses newest created_at (see program spec) |
| WM-POS-06 | Free delivery threshold | Any order_type, positive included fee, benefits_json has free_delivery_threshold_cents ≤ merchandise subtotal (after member + coupon, before tax), not customer-pays-shipping | delivery_charge = 0, original_delivery_charge unchanged; snapshot membership_free_delivery_waived true |
Automated coverage today includes parseAndValidateMembershipBenefitsJson, pos-membership-resolve.util rollup math, POS snapshot builder, and PosService.placeOrder waiver path — extend with integration tests when adding new enforced kinds.
Roadmap status
Section titled “Roadmap status”catalog_early_access has moved beyond storage-only:
- Phase A (visibility gating): implemented.
- Phase B (member preview price projection + UI rendering): implemented.
- Phase C (analytics + order traceability marker): implemented.
Deep design: catalog_early_access
Section titled “Deep design: catalog_early_access”Goal and scope
Section titled “Goal and scope”catalog_early_access should let vendors publish products or categories to members earlier than the public catalog while preserving:
- existing subscription resolution rules (vendor + user + store scope),
- current product status workflow,
- backward-compatible
benefits_jsonparsing.
This benefit is for catalog visibility and optional member pricing, not checkout discount math (that remains in order-level benefits).
Value schema (proposed contract)
Section titled “Value schema (proposed contract)”Current parser accepts object / string / null. To make behavior deterministic across backend + web, standardize on an object payload and keep legacy values as soft-compatible input:
{ "kind": "catalog_early_access", "value": { "mode": "allowlist", "starts_at": "2026-06-01T00:00:00.000Z", "ends_at": "2026-06-03T00:00:00.000Z", "category_ids": [12, 15], "product_ids": [1012, 1044], "member_price_override": { "type": "percent", "value": 10 } }}Field intent
mode:allowlist(only listed categories/products) orglobal_preview(all products in scope during window).starts_at/ends_at: UTC gate window. If omitted, window is always open while subscription is active.category_ids: category gates for preview.product_ids: explicit SKU gates.member_price_override: optional member-only preview price policy:type: percent(0–100),type: fixed_cents(minor units),- absent => no price override (visibility-only early access).
Resolution order (server-side)
Section titled “Resolution order (server-side)”For each catalog read request:
- Resolve effective membership subscription using existing rules (newest active/trialing for vendor + customer + store).
- Parse
catalog_early_accessfrom planbenefits_json. - Compute access window (
starts_at/ends_at). - Build allowed catalog set from
mode,category_ids,product_ids. - Apply optional
member_price_overrideto response projection (do not mutate base product price row).
When no valid benefit or no active subscription exists, fallback to current public catalog behavior.
API design notes (catalog endpoints)
Section titled “API design notes (catalog endpoints)”Keep endpoint path unchanged; gate via query context + auth:
- Vendor web customer context (selected customer on POS/catalog view) provides
user_id. - Backend returns only preview-eligible rows for member context during active window.
- Response should include explicit flags for UI:
is_member_preview: booleanmember_price_preview_cents?: numberpreview_window_ends_at?: string
This avoids implicit frontend guessing and supports badges/countdown UX.
UI/UX requirements (vendor web)
Section titled “UI/UX requirements (vendor web)”- Catalog cards/list rows:
- badge: Member Early Access,
- optional member price with crossed public price.
- Membership product modal:
- structured editor for
catalog_early_accessobject (no raw JSON required for common flow), - preview summary text generated from chosen scope/window.
- structured editor for
- POS/cart:
- if product added under early access context, snapshot should mark source as membership preview for audit (new snapshot key when implemented).
Data and compatibility
Section titled “Data and compatibility”- No new table required in phase 1 (derive from
benefits_json). - Keep parser backward compatibility:
- string/null accepted but treated as “disabled/no-op” unless mapped by migration policy,
- object is canonical for enforcement.
- Add shared typed model in
packages/shared/src/membership/membership-model.tsfor exact value shape.
Test matrix extension (for implementation phase)
Section titled “Test matrix extension (for implementation phase)”Add these IDs when coding starts:
WM-CAT-01: member with active window sees gated products; guest does not.WM-CAT-02: window expired => preview rows hidden.WM-CAT-03: store mismatch => no preview access even with active subscription.WM-CAT-04: category allowlist + explicit product allowlist union behavior.WM-CAT-05:member_price_override.percentcomputes preview price correctly and never below zero.WM-CAT-06: malformed object falls back safely (validation error on write, no runtime crash on read).
Rollout plan
Section titled “Rollout plan”- Phase A (visibility only): enforce gating, no price override.
- Phase B (member preview price): add response projection + UI badges/price.
- Phase C (analytics): track preview conversion and attach source marker in order snapshot.
Each phase must include unit tests in parser/resolver + integration tests for catalog listing contracts.
Implementation checklist by phase (execution runbook)
Section titled “Implementation checklist by phase (execution runbook)”This checklist maps docs design to concrete implementation touchpoints for backend + frontend.
Phase A — Visibility gating only
Section titled “Phase A — Visibility gating only”Backend scope
- Add canonical typed value contract for
catalog_early_accessinpackages/shared/src/membership/membership-model.ts. - Extend membership benefit parser/validator in
apps/backend/src/database/utils/membership-benefits-json.util.ts:mode,starts_at/ends_at,category_ids/product_ids.
- Add resolver utility in vendor catalog read flow to compute preview eligibility from:
- vendor + user + store subscription scope,
- active preview window,
- allowlist/global mode.
- Keep normal catalog response unchanged for non-member context.
Frontend scope (vendor web)
- Add optional member context input to catalog query flow (selected customer where applicable).
- Render preview badge when backend marks row as member-preview.
- Keep public price behavior unchanged (no override yet).
Required tests
- Unit: parser accepts valid object, rejects malformed object.
- Unit: resolver enforces store scope + window + allowlist union.
- Integration: same catalog endpoint returns different visibility for member vs guest context.
- Suggested target: add/extend POS catalog listing integration spec (backend) to assert:
- member context returns preview-only rows in active window,
- guest/no-user context hides those rows,
- store mismatch still hides preview rows.
- Pass gate: one spec covers both positive + negative visibility with deterministic fixtures.
- Implemented in:
apps/backend/src/modules/pos/pos.service.spec.ts.
- Suggested target: add/extend POS catalog listing integration spec (backend) to assert:
Done criteria
- Member can see preview-only items in active window.
- Guest/non-qualified member cannot see those items.
- No regression on existing catalog list for non-members.
Phase B — Member preview price
Section titled “Phase B — Member preview price”Backend scope
- Support
member_price_overridevalidation (percent/fixed_cents). - Compute
member_price_preview_centsin response projection (never mutate sourceprice). - Include explicit response flags:
is_member_preview,member_price_preview_cents,preview_window_ends_at.
Frontend scope (vendor web)
- Show crossed public price + member preview price.
- Show countdown/end hint using
preview_window_ends_at. - Keep fallback rendering stable when flags are absent.
Required tests
- Unit: price override math (
percent,fixed_cents, floor at 0). - Integration: API response includes preview flags for qualified rows.
- UI test: badge + preview price render correctly from API contract.
- Integration target (backend): assert row payload includes:
is_member_preview,member_price_preview_cents,preview_window_ends_atfor qualified member context only.
- UI target (vendor web): cover row/card rendering when:
- preview flags exist (badge + crossed price + preview price + end hint),
- preview flags are absent (fallback public rendering unchanged).
- Pass gate: response contract + render behavior both verified by automated tests.
- Implemented (backend integration):
apps/backend/src/modules/pos/pos.service.spec.ts. - Implemented (vendor-web UI contract mapping):
apps/vendor-web/e2e/pos-catalog-member-preview.spec.ts.
- Integration target (backend): assert row payload includes:
Done criteria
- Qualified member sees deterministic preview pricing.
- Non-qualified users never receive preview price fields.
Phase C — Analytics and order traceability
Section titled “Phase C — Analytics and order traceability”Backend scope
- Add event/metrics capture for preview impressions and conversions.
- Add order snapshot marker for early-access source when item purchased under preview context.
- Document new snapshot keys in membership docs if introduced.
- Implementation notes:
- Emit analytics events with stable names and vendor/store/customer dimensions:
pos_member_preview_exposurepos_member_preview_add_to_cartpos_member_preview_conversion
- Add snapshot key only when item came from member preview context (no-op for normal flow).
- Keep key naming aligned with existing
membership_*snapshot fields.
- Emit analytics events with stable names and vendor/store/customer dimensions:
- Implementation notes:
Frontend scope (vendor web)
- Track preview exposure events from catalog view.
- Track click/add-to-cart actions for preview items.
- Implementation notes:
- Fire exposure once per visible preview row set (avoid duplicate spam on rerender).
- Fire action events with product id + preview context + current customer context.
- Keep payload schema shared with backend analytics contract.
- Implementation notes:
Required tests
- Unit: analytics payload builder.
- Integration: order snapshot includes preview marker when applicable.
- Regression: normal purchase flow unaffected without preview context.
- Pass gate:
- payload builder test validates event schema and required dimensions,
- integration test asserts snapshot marker appears only for preview-origin purchase,
- regression test confirms non-preview orders keep existing snapshot behavior.
- Pass gate:
Done criteria
- Team can report preview-to-purchase funnel.
- Finance/support can audit preview-origin orders via snapshot.
When implementing, add unit tests next to the util or service, update this playbook table (posEnforced column), and update the vendor modal (data comes from shared VENDOR_CUSTOMER_MEMBERSHIP_BENEFIT_PLAYBOOK).
Phase C completion notes
Section titled “Phase C completion notes”- Analytics contract + payload builders:
apps/vendor-web/src/lib/pos-member-preview-analytics.util.ts. - POS event emit points:
- exposure on visible preview set,
- add-to-cart for preview item,
- conversion on successful order.
- UI rendering assertion expanded to full card check:
apps/vendor-web/e2e/pos-catalog-member-preview.spec.ts.
- Snapshot non-preview regression:
apps/backend/src/modules/pos/pos-place-order-snapshot.util.spec.ts.