Vendor membership program (spec)
Nội dung này hiện chưa có sẵn bằng ngôn ngữ của bạn.
Executive summary
Section titled “Executive summary”Vendor membership is a tenant-scoped program: each vendor owns a catalog of membership plans (membership_products, audience vendor_customer) and active subscriptions (membership_subscriptions) that link store customers to those plans. It is intentionally separate from admin SaaS vendor (platform) plans (platform_vendor).
The mental model is a chain retailer: one legal vendor account, many physical or logical stores, many end customers. Benefits can be defined vendor-wide or per store; checkout only applies benefits when the current POS store, logged-in customer, and plan + subscription scopes all align.
Operational detail lives in Membership products and Membership subscriptions; this page is the single-page spec tying scope, data, and POS behavior together.
Tenancy and actors
Section titled “Tenancy and actors”| Actor | Role |
|---|---|
| Vendor | Tenant key on plans and subscriptions (vendor_id). All CRM and POS resolution is under this boundary. |
| Store | Optional scope on a plan (membership_products.store_id) and inherited on subscriptions. POS uses the order’s store when matching. |
| Customer | users row associated with the vendor (user_id on subscription). POS passes user_id on place_order for membership resolution. |
Data model (high level)
Section titled “Data model (high level)”flowchart LR
subgraph vendor["Vendor (tenant)"]
MP["membership_products\n(plans)"]
MS["membership_subscriptions\n(customer ↔ plan)"]
U["Customer user"]
end
MP --> MS
U --> MS
MP -->|"store_id null = all stores"| S["Stores"]
MP -->|"store_id set"| S
MS -->|"store_id from plan"| MP
membership_products: sellable plan;audience = vendor_customer; optionalbenefits_json(array of typed benefit rows).membership_subscriptions: bindsuser_id+membership_product_idfor that vendor;store_idon the row follows the plan at create time (see Membership subscriptions — Store scoping under APIs).
Store scoping rules
Section titled “Store scoping rules”Plans and subscriptions use store_id to mean where the program applies.
Plan store_id | Meaning |
|---|---|
| NULL | Plan is not limited to one store; valid for any store of the vendor (chain-wide catalog entry). |
| Set | Plan is only for that store; employees must be allowed that store to manage it. |
At POS, the backend loads an open subscription with:
- Subscription
statusintrialingoractive. - Subscription
store_idis NULL or equals the checkout store ((ms.store_id IS NULL OR ms.store_id = :storeId)). - Plan
statusis active, samevendor_id,audience = vendor_customer, and planstore_idis NULL or equals the checkout store ((mp.store_id IS NULL OR mp.store_id = :storeId)).
Implementation reference: apps/backend/src/modules/pos/pos-membership-resolve.util.ts (loadActiveVendorCustomerMembershipSubscriptionForPos).
Which subscription wins (POS)
Section titled “Which subscription wins (POS)”If more than one row satisfies the query, TypeORM returns a single row ordered by membership_subscriptions.created_at descending — i.e. the newest qualifying subscription is used for benefit rollup. Vendors should avoid overlapping open subscriptions for the same customer + store + overlapping plans; when they exist, behavior follows that last-created rule.
Applied benefits at checkout (today)
Section titled “Applied benefits at checkout (today)”Only a subset of benefits_json kinds is enforced in POS (see Membership subscriptions — POS):
order_discount_percent— max percent across rows; after store-level discounts, before coupon.order_discount_fixed_cents— summed fixed cents; combined with percent; total membership discount capped to subtotal.loyalty_points_multiplier/loyalty_points_bonus_per_order— affect earn calculation.
Finance: membership discount flows into vendor expense / tax allocation as documented on the subscriptions page; orders.indochina_pos_place_snapshot may record membership_discount_amount, membership_subscription_id, membership_free_delivery_waived, membership_free_delivery_threshold_cents, membership_catalog_early_access_order, and membership_catalog_early_access_item_ids when discount > 0, free delivery was applied, or the order includes early-access catalog items under qualified membership preview context.
free_delivery_threshold_cents is enforced in POS for any order_type whenever a positive delivery_charge is included and the customer is not on customer pays shipping (see Membership subscriptions — POS). catalog_early_access is now enforced in POS (visibility + preview pricing + snapshot marker), and vendor-web emits analytics events:
pos_member_preview_exposurepos_member_preview_add_to_cartpos_member_preview_conversion
price_cents and duration_days on membership_products are catalog / billing metadata (list price and term length). They are not stored in benefits_json. POS custom membership lines must match price_cents; the benefit rollup still ignores them for discount math.
catalog_early_access implementation contract
Section titled “catalog_early_access implementation contract”This section documents the normalized contract used by backend + vendor web. The contract is implemented and remains the source of truth for future extensions.
Canonical benefits_json value shape
Section titled “Canonical benefits_json value shape”{ "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 } }}Validation rules (write path)
Section titled “Validation rules (write path)”valuemust be an object for enforceable behavior.mode:allowlist|global_preview.starts_at/ends_at: ISO datetime, UTC; if both present thenends_at > starts_at.category_ids/product_ids: integer arrays, deduplicated, max size bounded (protect query fan-out).member_price_override(optional):type:percent|fixed_cents,percentin range0..100,fixed_cents >= 0.
Legacy string | null inputs may remain accepted for backward compatibility, but must be treated as non-enforced/no-op values until migrated.
Resolution rules (read path)
Section titled “Resolution rules (read path)”For catalog read endpoints under member context:
- Resolve effective subscription (same vendor/store/user constraints and newest-row rule as POS membership resolver).
- Parse
catalog_early_access. - Check active time window (
starts_at/ends_at). - Build eligible set by:
global_preview: all products in vendor/store scope,allowlist: union of listedcategory_idsandproduct_ids.
- Apply optional
member_price_overrideas response projection only.
If any step fails, fall back to normal public catalog behavior (no hard failure for end user listing).
API response contract (catalog)
Section titled “API response contract (catalog)”When product is returned because of early access, include explicit fields:
is_member_preview: booleanmember_price_preview_cents?: numberpreview_window_ends_at?: string
These flags are required for deterministic frontend rendering (badge, countdown, member price) and avoid duplicated business logic in UI.
Shared typing + test obligations
Section titled “Shared typing + test obligations”- Add shared typed model for this value in
packages/shared/src/membership/membership-model.ts. - Reuse shared enum/tuple for:
catalog_early_access.mode,member_price_override.type.
- Required automated coverage when implemented:
- parser/validator unit tests,
- resolver unit tests (store scope + window + allowlist logic),
- endpoint integration tests for catalog visibility and response flags.
For rollout and product UX checklist, see Membership playbook.
Preconditions for POS application
Section titled “Preconditions for POS application”Membership discount / loyalty rollup runs only when:
place_orderincludes a non-emptyuser_id(logged-in store customer on the order).- The order has a real store context (
storeIdand vendor on the store) so tenant + store filters apply.
If the customer checks out guest or user_id is missing, no vendor-customer membership resolution runs (even if a subscription exists in CRM).
Module and surface area
Section titled “Module and surface area”- Vendor dashboard: module
customer— Customers, Membership products, profile Membership subscriptions (see subscriptions guide). - APIs: vendor JWT, paths under
/vendor/membership-productsand/vendor/membership-subscriptions(client modules inapps/vendor-web/src/services/vendor-membership-*.api.ts).
Related
Section titled “Related”- Membership playbook — retail mapping, rollout checklist, QA matrix
- Membership products (Vendor) — plan CRUD and
benefits_json - Membership subscriptions (Vendor) — assign/cancel, statuses, API errors, POS benefit table
- Customers (Vendor) — CRM entry point
- Membership products (Admin) — platform vendor (SaaS) catalog — different audience