Membership subscriptions (Vendor)
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”Membership subscriptions bind a store customer (users row for your vendor) to a membership plan (audience vendor_customer). This is operational CRM data on the vendor dashboard; it is not the same as admin SaaS vendor plans (platform_vendor).
This feature records who is subscribed to which plan and supports cancel via API/UI. POS applies a subset of plan benefits automatically when a matching subscription exists (see below).
POS: applied benefits (checkout)
Section titled “POS: applied benefits (checkout)”When place_order runs with a logged-in customer (user_id), the backend loads the newest trialing or active subscription for that vendor + user, scoped by subscription store_id (null = all stores) and by the plan (vendor_customer, active, same vendor, optional plan store_id).
From the plan’s benefits_json, the POS pipeline uses:
| Benefit kind | Effect |
|---|---|
order_discount_percent | Max percent among rows; applied to the order subtotal after store-level discounts, before coupon. |
order_discount_fixed_cents | Sum of fixed cents, converted to currency; combined with percent; total membership discount capped to that subtotal. |
loyalty_points_multiplier | Max multiplier; earn points uses (\lfloor \text{order_amount} \times \text{LOYALTY_EARN_RATE} \times \text{multiplier} \rfloor + \text{bonus}). |
loyalty_points_bonus_per_order | Sum of integer bonuses added after the floored earn. |
free_delivery_threshold_cents | Minimum merchandise subtotal in USD cents (after membership + coupon, before tax and delivery) to waive the included delivery_charge. Applies for any order_type when the charge is positive, the customer is not on customer pays shipping (customer_pays_shipping on the delivery payload when present), and the plan row is a non‑negative integer. If multiple rows exist, the lowest threshold wins (easier free delivery). |
Finance: membership discount is included in order_transactions.store_expense (vendor subsidy) and recorded as an expenses row (DISCOUNT_ON_PRODUCT, description POS membership plan subsidy). Tax allocation treats membership like other order-level discounts (orderLevelDiscountPoolDollars). Snapshot: orders.indochina_pos_place_snapshot may include membership_discount_amount, membership_subscription_id, membership_free_delivery_waived, membership_free_delivery_threshold_cents, membership_catalog_early_access_order, membership_catalog_early_access_item_ids when discount > 0, free delivery was applied, or the order contains catalog early-access items under qualified preview context.
To support fast benefit-history queries (without N+1 order-detail reads), successful POS orders now also write one precomputed row into membership_benefit_orders when membership benefits were applied. The row includes membership discount, free-delivery savings, and loyalty bonus benefit (extra points converted to money-equivalent with the current loyalty redeem rate), plus order totals including total_tax_amount so UI can compute savings-rate against the order amount excluding tax.
total_savings_amount is the cash-equivalent savings only (membership_discount_amount + free_delivery_savings_amount).
loyalty_savings_amount is tracked separately for point-equivalent value and is not added into total_savings_amount.
For order detail / receipt parity, vendor order APIs (/vendor/orders and /vendor/orders/:id) also expose precomputed fields from membership_benefit_orders:
membership_benefit_total_savings_amountmembership_benefit_discount_amountmembership_benefit_free_delivery_savings_amountmembership_benefit_loyalty_savings_amount
This keeps membership savings displayed on order detail and printable receipt aligned with the saved benefit row.
catalog_early_access is enforced in POS (visibility gate + member preview price projection + order snapshot marker) and emits UI analytics events for funnel tracking:
pos_member_preview_exposurepos_member_preview_add_to_cartpos_member_preview_conversion
Implementation details and QA checklist live in the membership playbook.
Selling a plan on POS (place_order)
Section titled “Selling a plan on POS (place_order)”When the cart includes a custom line with membership_product_id, the order must identify the buyer: send a non-empty user_id (store customer). Walk-in / guest checkout (user_id omitted or null) is rejected with membership_requires_customer. At most one membership plan line is allowed per order (membership_cart if more than one).
For non-draft orders, after lines are inserted the backend creates an active membership_subscriptions row for that user_id and plan (same rules as manual assignment: vendor match, active vendor_customer plan, no duplicate open subscription). Notes reference the order id (pos_order:<orders.id>).
Vendor web: the Membership plan action is shown only when a real customer is selected; switching to walk-in clears any membership lines from the cart.
Where it appears in the app
Section titled “Where it appears in the app”- Customer profile (
/dashboard/customers/:id) — section Membership subscriptions: list, assign a plan, cancel (with confirmation). - Subscriptions list page (
/dashboard/membership-subscriptions) — centralized table for all subscription rows (search, status tabs, pagination). - Uses module
customer(same as Customers):VendorJwtAuthGuard,VendorModuleGuard,@RequireVendorModule('customer').
Database and deployment
Section titled “Database and deployment”- Table:
membership_subscriptions(migration1780060000000-CreateMembershipSubscriptionsTable). - Table:
membership_benefit_orders(migration1781300000000-CreateMembershipBenefitOrdersTable) for precomputed per-order membership savings. - Apply migrations on each environment (dev/staging/production) before using the UI or APIs; otherwise subscription features will fail or return
vendor_requires_databasewhen DB repositories are unavailable. - Backend requires
DB_ENABLED=true(or equivalent) for TypeORM-backed subscription operations; without a DB connection, the service returns 503 withcode: vendor_requires_database.
Subscription statuses
Section titled “Subscription statuses”Aligned with MEMBERSHIP_SUBSCRIPTION_STATUS_VALUES in the backend / shared model:
| Status | Meaning (typical use) |
|---|---|
draft | Record exists but not fully live |
trialing | Trial period |
active | Current membership |
past_due | Billing / renewal issue (future use) |
canceled | Ended by vendor or process; canceled_at set when status moves to canceled |
expired | Natural end (e.g. past ends_at) |
Create default: if status is omitted, the API defaults to active.
End date (ends_at): if ends_at is omitted on create, the API sets it from the plan’s membership_products.duration_days (integer ≥ 1): ends_at = starts_at + N × 24h (same as the vendor-web display fallback). Sending ends_at: null explicitly keeps the subscription open-ended (no auto duration).
One open membership per customer: you cannot create another subscription for the same vendor + customer while any row exists in draft, trialing, active, or past_due (409 customer_open_membership_exists). canceled / expired rows do not block a new assignment.
APIs (web client)
Section titled “APIs (web client)”Implemented in apps/vendor-web/src/services/vendor-membership-subscriptions.api.ts. Base path: /vendor/membership-subscriptions (Bearer vendor JWT).
| Method | Path | Purpose |
|---|---|---|
| GET | /vendor/membership-subscriptions | List — query: page, limit (max 100), user_id, membership_product_id, status |
| GET | /vendor/membership-subscriptions/benefit-history | List precomputed savings rows — query: page, limit (max 100), user_id, membership_subscription_id |
| GET | /vendor/membership-subscriptions/:id | One subscription |
| GET | /vendor/membership-subscriptions/:id/benefit-history | List precomputed savings rows for a specific subscription id |
| POST | /vendor/membership-subscriptions | Create — body: user_id, membership_product_id, optional status, starts_at, ends_at, notes |
| PATCH | /vendor/membership-subscriptions/:id | Update — optional status, ends_at, notes. Setting status: canceled sets canceled_at if not already set; active / trialing clears canceled_at |
Store scoping: store_id on the saved subscription is taken from the membership product (plan). If the plan has a store, the current vendor user must be allowed that store (employee scoping); otherwise 403 store_not_allowed.
Common API errors (structured body)
Section titled “Common API errors (structured body)”Responses often use { success: false, errors: [{ code, message }] }.
| HTTP | code | When |
|---|---|---|
| 404 | user_not_found | Customer id unknown |
| 403 | user_not_in_vendor | User is not a customer of this vendor |
| 404 | product_not_found | Plan id unknown |
| 403 | invalid_product_audience | Plan is not vendor_customer |
| 403 | product_not_in_vendor | Plan belongs to another vendor |
| 409 | product_inactive | Plan status is not active |
| 409 | customer_open_membership_exists | Customer already has an open membership (any plan) |
| 403 | store_not_allowed | Employee cannot access plan’s store |
| 404 | subscription_not_found | Wrong id or other vendor’s row |
| 503 | vendor_requires_database | DB not available / repos not injected |
Validation errors (bad status, pagination, etc.) return 400 via the global validation pipe.
Test scenarios (UAT / QA)
Section titled “Test scenarios (UAT / QA)”| ID | Scenario | Expected |
|---|---|---|
| MS1 | Assign active plan to customer | Row appears; list filtered by user_id shows it |
| MS2 | Assign a second plan while first is active (or draft / trialing / past_due) | 409 customer_open_membership_exists |
| MS3 | Assign inactive plan | 409 product_inactive |
| MS4 | Cancel from UI | PATCH canceled; canceled_at populated |
| MS5 | Employee without store access | 403 when plan is store-scoped to forbidden store |
Related
Section titled “Related”- Vendor membership program (spec) — tenancy, store scope, which subscription wins at POS
- Membership playbook — retail mapping, rollout checklist, test matrix
- Customers (Vendor) — profiles and list
- Membership products (Vendor) — plan catalog
- Membership products (Admin) — platform vendor (SaaS) catalog
- Vendor panel overview — module
customer