Customers (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”Customers provides list (filters, sort, pagination, KPIs) and profile detail. Module customer applies; mutations need write where route rules say so.
Goals and non-goals
Section titled “Goals and non-goals”| Goals | Non-goals |
|---|---|
| CRM-style store customer ops | Email marketing automation |
| Respect PII handling | Sync to external CRM without integration |
Current platform behavior
Section titled “Current platform behavior”- Prefix
/dashboard/customers→ modulecustomer; owners bypass checks. - APIs documented in-page (APIs section).
Sequence: happy path
Section titled “Sequence: happy path”- Open Customers → filter → open profile.
- Create/update per UI if enabled and write allowed.
Security and operations
Section titled “Security and operations”- PII — minimize export; comply with local privacy law.
Test scenarios (UAT / QA)
Section titled “Test scenarios (UAT / QA)”| ID | Scenario | Expected |
|---|---|---|
| CU1 | List KPI + rows | Loads for allowed module |
| CU2 | Detail | Shows customer fields |
| CU3 | Employee read-only | Mutations disabled or API 403 |
| CU4 | Customer category: set wholesale defaults; assign customer to category | Create-order / wholesale-credit-status reflects category → store merge |
| CU5 | Customer wholesale overrides | Overrides win over category and store |
Routes
Section titled “Routes”/dashboard/customers— list with filters, sort, pagination, KPI overview/dashboard/customers/:id— profile detail/dashboard/customers/categories— customer segments / labels (CRMcustomer_categories): list, create/edit modal, import/export Excel. Used to group customers and to set wholesale (B2B) credit defaults per segment (see below).
Access (RBAC)
Section titled “Access (RBAC)”Prefix /dashboard/customers maps to module customer. Mutations (create/update/delete) require write where enforced by route rules. Owners always pass.
Implemented in apps/vendor-web/src/services/vendor-customers.api.ts.
List and overview use the auth context base path /auth/vendor/context (see VENDOR_AUTH_CONTEXT_API in lib/api.ts):
GET /auth/vendor/context/customers/overviewGET /auth/vendor/context/customers— query:page,limit,search,status,store_id,sort, date range, etc.GET /auth/vendor/context/customers/:id/loyalty— loyalty summary (loyalty_points,tier_key).GET /auth/vendor/context/customers/:id/loyalty-transactions— loyalty ledger (paginated).GET /auth/vendor/context/customers/:id/wallet— wallet summary (wallet_balance).GET /auth/vendor/context/customers/:id/wallet-transactions— wallet ledger (paginated).POST /auth/vendor/context/customers/:id/wallet/adjust— manual wallet credit. Body:amount,note,credit_type(cash|bank_transfer|check|card|other), optionalevidence_paths(up to 5 vendor gallerypublic_pathvalues, e.g. photos of checks or transfer receipts). Ledger row usestransaction_type = vendor_manual_creditand JSONmetadata(credit_type,evidence[]).GET /auth/vendor/context/customers/segments/overview— segmentation counters (vip,loyal,new,dormant, plus tier counters).GET /auth/vendor/context/customers/:id/timeline— blended customer timeline (orders + loyalty events, newest first).GET /auth/vendor/context/customers/crm/kpis— CRM performance KPIs (repeat_rate,average_order_value,open_membership_customers,memberships_expiring_soon,redemption_rate_30d, …).GET /auth/vendor/context/customers/:id— detail (same:idsemantics asGET /api/v1/vendor/customers/:id).idis always CRMcustomers.id(same value as list rows and Excel export). The row must fall in an authorized store, or vendor owners may load CRM rows withstore_idnull for that vendor.
Wholesale (B2B) credit — status preview
Section titled “Wholesale (B2B) credit — status preview”GET /vendor/customers/:id/wholesale-credit-status?store_id=— returns resolved effective wholesale credit policy for that CRM customer in the given store, plus open/overdue wholesale debt round counts and a placement preview (whether a new wholesale debt order would warn or be denied).store_idis required; must be in the vendor’s authorized stores.
Create/update/delete call /vendor/customers directly:
POST /vendor/customers— create (optionalstore_idto scope the customer to a store that belongs to the vendor). Uniqueness:emailandphoneare enforced per vendor (and legacy rows tied only by store within the vendor’s stores), so the same email or phone may exist for customers of other vendors. Optional body fields mirror Laraveluserscolumns for extended profile:bussiness_name,bussiness_type,city,street_address,state,zip_code,ref_code,limit_amount_debt,limit_order_amount_owed,maximum_amount_debt. Duplicate email/phone for the same vendor returns 409 with{ success: false, errors: [{ code, message }] }(not a generic 500).GET /vendor/customers/:id— detail;:idis CRMcustomers.idonly (matches list/export).PATCH /vendor/customers/:id—:idis CRMcustomers.id. Excel id column is the same. Rows matched only by phone must already have a CRM row for that retail login (otherwise import reports an error for that row). Login-backed creates that write a CRM mirror returncustomers.idas the public identifier for detail/PATCH. For CRM-only rows, sendingcreate_user: truewithemailandpasswordcreates and links a retail login to the existing CRM customer; status updates are limited to active (1) or blocked (0). Optionalcategory_id(CRM segment id) links the row to customer categories for wholesale defaults.
Optional wholesale (B2B) credit overrides on create/patch (nullable; omitted = no change):
wholesale_credit_term_mode—0none,1N days after invoice,2fixed day of next month.wholesale_credit_deadline_days— used when mode is1(positive integer).wholesale_credit_due_day_of_month— used when mode is2(1–28).wholesale_max_open_debt_rounds— cap on concurrent open wholesale debt “rounds” (receivable-based; not a money cap).- Sending explicit
nullon patch clears that override column so the customer inherits from category and/or store again.
Customers may reference a category_id (CRM segment). Effective policy resolution is documented in Wholesale (B2B) credit policy.
DELETE /vendor/customers/:idPOST /vendor/customers/:id/loyalty/adjust— manual loyalty adjustment (delta, optionalreason).
Customer categories API
Section titled “Customer categories API”Implemented in apps/vendor-web/src/services/vendor-customer-categories.api.ts.
GET /vendor/customer-categories— paginated list (page,limit,search,store_id).POST /vendor/customer-categories— create (name, optionalstore_id, description, image, plus optional wholesale credit fields below).GET /vendor/customer-categories/:idPATCH /vendor/customer-categories/:idDELETE /vendor/customer-categories/:idPOST /vendor/customer-categories/batch/delete— max 200 idsGET /vendor/customer-categories/export— Excel (columns include base label fields; wholesale columns are persisted on the entity for API/UI)POST /vendor/customer-categories/import— Excel import (existing behavior for name/store/description/image; wholesale fields are not part of the simple import sheet—set them in the UI or via PATCH)
Optional wholesale credit defaults for the segment (same semantics as customer overrides; null clears a stored default):
wholesale_credit_term_mode,wholesale_credit_deadline_days,wholesale_credit_due_day_of_month,wholesale_max_open_debt_rounds,wholesale_max_open_debt_rounds_rule(0allow /1warn /2deny when max open rounds would be exceeded on a new wholesale debt order).
DB: Migration adds prefixed columns on customer_categories (e.g. 1782900000000-CustomerCategoryWholesaleCredit).
Wholesale (B2B) credit policy
Section titled “Wholesale (B2B) credit policy”Applies only to wholesale sales (OrderSaleType.Wholesale / sale type 1) when the order is recorded on account (debt). Retail checkout does not use store debt caps (ops_max_debt_amount, ops_max_customer_debt_amount, sale_debt_over_limit_rule).
Store debt caps (Operations + Sales): on wholesale debt placement, evaluatePosStoreDebtPlacement compares the new unpaid amount and projected customer total against ops_max_debt_amount / ops_max_customer_debt_amount, using existing wholesale-only open receivable totals. sale_debt_over_limit_rule (allow / warn / deny) controls whether over-limit placement is blocked or only warned. Preview: GET /vendor/customers/:id/wholesale-credit-status?store_id=&order_outstanding= (optional projected unpaid amount).
Precedence (effective values):
- Customer — non-null wholesale credit fields on
customerswin for that field. - Customer category — for any field still unset on the customer, use non-null values on
customer_categorieslinked bycustomers.category_id. - Store —
store_configurationsprovides store-wide defaults for remaining fields.
Term modes: 0 no automatic deadline rule, 1 due N days after invoice date, 2 due on day X of the next calendar month (X clamped to last day of that month when needed).
Overdue wholesale receivables: always produce a warning in placement checks; they do not block checkout by themselves.
Max open debt rounds: if placing a new wholesale debt order would push the customer above the configured max, behavior is driven by wholesale_max_open_debt_rounds_rule: allow (no block), warn, or deny (default at store level is typically deny; category/customer can override the rule).
Implementation references: resolveWholesaleCreditPolicy / evaluateWholesaleCreditPlacement / computeWholesalePaymentDeadline in apps/backend/src/modules/pos/wholesale-credit-policy.util.ts; POS enforcement in PosService.placeOrderWithStock; shared constants in packages/shared/src/constants/wholesale-credit.ts.
Vendor UI: store defaults under Stores → Configuration; category defaults in Customers → Categories modal; per-customer overrides in add/edit customer modals and on create wholesale order (credit status query + banners).
Loyalty (Phase 1 MVP)
Section titled “Loyalty (Phase 1 MVP)”- Auto earn: when a POS order is marked as paid (manual mark success or terminal webhook success), earn applies to
customers.loyalty_pointwhen the order has a CRMcustomer_id, otherwise tousers.loyalty_point. The ledger row usestransaction_type = "earn"andreference = "order:<order_id>"; CRM-path rows may haveloyalty_point_transactions.user_idnull. - Auto reversal on refund: when terminal webhook marks an order as refunded, earned points for that order are reversed with
transaction_type = "refund_reversal". - Idempotency: earn/reversal uses the order reference key to prevent duplicate credits/debits from repeated callbacks.
- Vendor visibility: customer detail exposes loyalty summary and paginated loyalty transactions.
GET …/loyalty-transactionsandGET …/timelineinclude CRM-linked ledger rows (matchingreferencetoordersfor thiscustomers.idin authorized stores), not only rows keyed by a linked retailusers.id. - Wallet refund credit: when a refund is moved to
refundedandrefund_method = "wallet", backend creditsusers.wallet_balanceand writes awallet_transactionsledger row withtransaction_type = "order_refund".
Loyalty + CRM (Phase 2)
Section titled “Loyalty + CRM (Phase 2)”- Point redemption on checkout: POS accepts optional
loyalty_points_redeemin place-order payload and applies redemption once payment is captured. - Redeem idempotency: redemption writes a ledger row with
transaction_type = "redeem"andreference = "order:<order_id>"; repeated paid callbacks do not double-debit. - Refund restore: when refunded, redeemed points are restored with
transaction_type = "redeem_reversal"(alongside earn reversal). - Segmentation API: vendor can query
segments/overviewfor quick CRM targeting in dashboard. - Customer timeline API: customer detail can render unified history across order and loyalty events.
CRM Reporting (Phase 3)
Section titled “CRM Reporting (Phase 3)”- CRM KPI API: backend exposes aggregated metrics for vendor dashboard:
purchasing_customers,repeat_customers,repeat_rateaverage_order_valueopen_membership_customers— distinct customers with at least one membership subscription indraft,trialing,active, orpast_duefor the scoped store(s)memberships_expiring_soon— open-status subscriptions withends_atset, ending within the next 30 days (same store scope)redemption_customers_30d,redemption_rate_30d
- Dashboard visibility: vendor customer list page consumes
overviewpluscrm/kpisand shows four KPI cards: total customers (MoM trend), average order value, membership customers, and memberships expiring soon. - Store scope: all KPI endpoints respect vendor store authorization and optional
store_idfilter.
Treat customer PII (name, email, phone, addresses) according to your organization’s privacy policy; the UI is a standard dashboard CRUD surface.
Database: global unique on users
Section titled “Database: global unique on users”If the legacy schema still has global unique indexes on users.phone / users.email (common Laravel names users_phone_unique, users_email_unique), inserts can fail even when another vendor already uses that phone or email. Migration 1778200000001-DropUsersGlobalPhoneEmailUniqueIndexes drops those index names on the prefixed users table when they exist (MySQL information_schema). If your database uses different index names, adjust or drop them manually, then add composite uniques (vendor_id, phone) / (vendor_id, email) if your DBA requires DB-level enforcement.
Linking to orders and receivables
Section titled “Linking to orders and receivables”- Orders: customers appear on order list/detail via the orders APIs (Orders (Vendor)).
- Receivables: finance receivable lines reference
user_idandorder_id(Finance).
Related
Section titled “Related”- Membership products (Vendor) — Customer membership (catalog under Customers).
- Membership subscriptions (Vendor) — assign plans to customers; list/cancel on customer profile.
- Finance — receivables list.
- Vendor panel overview — module
customer.