Pricing & promotions
Customer groups and wholesale pricing
Generated from docs/plan/core/customer-groups-and-wholesale-pricing/PLAN.md in the AstroBaaS repository. Nothing described below is implemented — it is the written plan for it.
Core feature: merchants need to segment customers (members, resellers, loyal) and apply group-specific tax classes and pricing lookups. This free feature adds the CustomerGroup entity, customer-to-group assignment, and cascading group into tax and pricing subsystems—foundation for all group-based features.
The problem
No customer groups exist. Merchants cannot segment buyers (members vs. public, resellers vs. retail, loyalty tiers). Tax classes cannot be assigned per group. Tier pricing and volume breaks cannot reference groups. Gift cards and subscriptions cannot use groups as eligibility criteria.
What it does
- Define customer groups with name and optional description
- Assign customers to groups (one group per customer, nullable)
- Cascade group’s tax class into order tax calculation (if group has tax_class_id set and customer is in group, use group’s tax class)
- Store and retrieve customer’s group in customer detail view
- Filter/search customers by group in admin list
- Cascade group into tier-pricing lookups: paid packs (smart-promotions, b2b-wholesale) check customer’s group when evaluating pricing rules
- Display group on customer detail page with quick-assign dropdown
What it deliberately does NOT do
Each boundary carries its reason. A boundary without a reason gets crossed by the next person who reads this.
- Does not implement pricing tiers themselves—that’s the responsibility of smart-promotions-engine or b2b-wholesale-tiered-pricing features; groups are the structure only
- Does not auto-move customers between groups based on spend or behavior—business logic, manual assignment or via merchant’s admin script
- Does not gate features/storefront content by group membership (e.g., show ‘members-only’ products)—that’s access control, separate from groups
- Does not provide group-based email segments or marketing automation—that’s a marketing feature, not CMS
- Does not implement group-specific shipping costs—shipping is a separate subsystem; groups can inform shipping decisions but don’t own the logic
- Does not support hierarchical groups (parent/child)—flat structure keeps schema and assignment logic simple
Data model
New entity: CustomerGroup with name (string, unique), description (text, nullable), tax_class_id (nullable FK to TaxClass). New field on Customer: group_id (nullable FK to CustomerGroup). No migration data: all existing customers have group_id=null; groups are created by merchant post-launch. Add index on Customer.group_id for filtering.
API
- POST /admin/api/customer-groups — create group with name and optional tax_class_id
- GET /admin/api/customer-groups — list all groups (paginated)
- PUT /admin/api/customer-groups/:id — update group name or tax_class_id
- DELETE /admin/api/customer-groups/:id — delete group (fails if customers assigned)
- PUT /admin/api/customers/:id/group — assign or remove customer from group (POST body { group_id: id or null })
- GET /admin/api/customers?group=:id — list customers in group
- GET /api/customer/group — GET current customer’s group { id, name, tax_class_id } (authenticated only; anonymous returns null)
Every route added here must also appear in src/pages/openapi.json.ts — a test
fails the build if it does not.
Admin
Customer groups management page: CRUD interface, display count of customers per group, warn before deleting group if customers assigned. Customer detail view: shows current group with dropdown to assign/unassign. Customers list page: add ‘Group’ column, filterable by group dropdown. On group delete: show list of customers assigned and count; require explicit confirmation.
The seam — why this is core
Core owns: customer entity, group CRUD, assignment logic, group display. Core also owns: cascading group’s tax_class_id into order tax calculation (because tax is a core concern). Paid packs own: pricing logic that uses groups (e.g., volume breaks per group, tier pricing per group, loyalty tiers per group). Why: groups are a structural requirement that all merchants need (market segmentation is universal); pricing rules that USE groups are optional business logic that can be added piecemeal.
Part of base pricing domain. Cascades into no wholesale pricing, no member pricing, no group tax class—all free in Magento.
Dependencies
- TaxClass entity must exist (co-planned or pre-existing) to FK from CustomerGroup
- Customer entity must be schema-extensible to add group_id column
- Tax calculation hook in order checkout must support group-based tax class override
Acceptance checks
Each of these must be able to fail. Before claiming this is done, break the code deliberately and watch each one go red.
- Create group ‘members’; assign 5 customers to it; List via /admin/api/customers?group=:id returns all 5
- Assign same customer to group ‘loyal’; prior ‘members’ assignment is replaced; customer now appears only in ‘loyal’ group
- Delete group with 0 customers: DELETE succeeds. Delete group with 5 customers: DELETE fails with ‘Cannot delete group with assigned customers’
- Tax class cascading: group has tax_class_id=123 (VAT 20%), customer in that group; new order from that customer uses tax_class_id=123 (not customer’s personal tax class if different)
- GET /api/customer/group returns { id, name, tax_class_id } for authenticated customer; anonymous GET returns null
- Filter customers by group: admin filters by ‘members’ group, list shows only customers with group_id matching ‘members’
- Create group with duplicate name; request fails with ‘Name already exists’
Risks
If tax class cascading is optional or weak, merchants won’t know that group membership affects tax calculation—add explicit flag enable_tax_cascade on CustomerGroup or document prominently. Deleting a group that has customers requires strict enforcement; query must check for assigned customers before DELETE allowed. Customer.group_id can be null—all queries and code paths must gracefully handle nulls. Group assignment order of operations: if customer has order in process when group is changed, which tax class applies—the one at cart build time, or at payment time? Define and test.
Commercial context
| Suggested price | Core |
| Rival anchor | Magento Open Source: customer groups ship free, drive tax class and tier pricing. |
The anchor is what the nearest equivalent charges on Shopify or Magento today. It is context for a pricing decision, not the decision.
Generated from the commerce plan. See docs/COMMERCE-PLAN.md for the full
catalogue and ../../AI-GUIDE.md for how to work on this repository.