Pricing & promotions
Smart Promotions Engine
Indicative price, not an offer: EUR 300-500/year or EUR 600 one-off
Generated from docs/plan/paid/smart-promotions-engine/PLAN.md in the AstroBaaS repository. Nothing described below is implemented — it is the written plan for it.
Complex promotional deals—free gifts, volume breaks, conditional discounts—require custom code each time. This paid feature adds a visual rule builder with conditions (cart subtotal, product presence, qty, customer group, time windows) and actions (percentage discount, fixed discount, free gift, buy-X-get-Y), plus conflict resolution and analytics.
The problem
Marketing teams design complex deals (e.g., ‘Buy 3+ of any shirt, get 10% off total’ or ‘Free gift with orders over €100 in Dec’) but merchants must code each promotion. No visual builder exists. No rule engine. Merchants lose revenue opportunities because promotions take weeks to implement.
What it does
- Define promotion rules: conditions + actions with priority and time windows
- Condition types: min subtotal, product presence, qty threshold, customer group, day of week, is_first_purchase
- Action types: percent_discount, fixed_discount, free_gift, buy_x_get_y
- Apply promotions to cart at checkout with conflict resolution (priority-based)
- Promo code generation: merchant creates unlimited codes, optionally capped (max_uses, expiry_date)
- Apply promo code at checkout via API
- Promotion time windows: start_date, end_date, start_time, end_time (optional)
- Admin UI to create/edit/test promotions visually
- Promotion analytics: which promotions are used, revenue impact, average discount per transaction
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 support location-based promotions (e.g., ‘free shipping in CA only’)—that’s regional logic, separate from promotions
- Does not auto-generate promo codes (e.g., ‘generate 1000 unique codes’)—merchant creates codes manually or via admin UI batch generator
- Does not build A/B testing framework (e.g., show 10% vs. 15% to different users)—that’s analytics/marketing feature
- Does not integrate with loyalty programs (e.g., ‘loyalty members get extra 5% off’)—loyalty is separate; promotions can check loyalty tier if it exists
- Does not provide email campaign integration—that’s marketing automation, outside CMS scope
- Does not support tiered bundles (e.g., ‘buy any 3 items for $X’)—use explicit bundle products instead; promotions can discount bundles
Data model
New entity: Promotion with name, description, active (boolean), priority (integer), start_date, end_date, start_time (time, nullable), end_time (time, nullable). New entity: PromotionCondition with promotion_id, condition_type (enum: min_subtotal, product_present, qty_threshold, customer_group, day_of_week, is_first_purchase), value (JSON, e.g., {min_amount: 10000, product_id: ‘abc’, qty: 3, group_id: ‘xyz’}). New entity: PromotionAction with promotion_id, action_type (enum: percent_discount, fixed_discount, free_gift, buy_x_get_y), value (JSON, e.g., {percent: 10}, {amount: 500}, {product_id, qty}, {buy_qty: 3, get_product_id, get_qty: 1}). New entity: PromotionCode with code (unique), promotion_id (FK), active (boolean), max_uses (nullable), uses_count (integer default 0), expiry_date (nullable, timestamp). New field on Order: applied_promotion_ids (array of promotion IDs stored as JSON list or comma-separated string).
API
- POST /admin/api/promotions — create promotion with conditions and actions
- GET /admin/api/promotions — list all promotions (active/inactive, paginated)
- PUT /admin/api/promotions/:id — update promotion settings, conditions, actions
- DELETE /admin/api/promotions/:id — delete promotion
- POST /admin/api/promotions/:id/test — test promotion on sample cart (POST body { items, subtotal, customer_group }; returns promotions that would apply and discount amount)
- POST /admin/api/promotions/:id/codes — create batch of promo codes (POST body { quantity, code_prefix, max_uses_per_code, expiry_date })
- GET /admin/api/promotions/:id/codes — list all codes for promotion with usage stats
- PUT /admin/api/promotions/:id/codes/:code/deactivate — deactivate single code
- POST /store/api/cart/apply-promotion — apply promo code to cart (POST body { code }; returns updated cart with discount applied)
- GET /admin/api/promotions/:id/analytics — view promotion usage: times applied, total revenue discounted, average discount per order, top demographics
Every route added here must also appear in src/pages/openapi.json.ts — a test
fails the build if it does not.
Admin
Promotion builder: CRUD with visual condition/action editor. Add conditions via dropdown (type), fill values (amount, product, qty, etc.). Add actions via dropdown, fill values (%, amount, gift product, etc.). Set priority (higher = evaluated first). Set time window (start/end dates and optional times). ‘Test this promotion’ form: enter sample cart and customer group, click ‘Test’, see whether promotion applies and what discount is. Promotion code manager: view codes for promotion, show max_uses, current uses_count, expiry date. Batch-generate codes: specify quantity, code prefix, max uses, expiry. Deactivate codes. Analytics dashboard: pie chart of top promotions by usage, bar chart of revenue impact, scatter plot of average discount by promotion.
The seam — why this is paid
Core owns: cart entity, order entity, price-calculation interface. Paid pack owns: promotion rule engine (condition evaluation logic), action executor, conflict resolution, analytics. Why: promotion engine requires marketing domain expertise (how to resolve conflicting promotions, when to apply stacked discounts, etc.); merchants often hire consultants to design promotion strategy; this is business-logic premium support.
Requires marketing automation and business logic expertise. Magento extension model: per-installation one-off, not per-transaction.
Dependencies
- Cart system must exist and be queryable (items, subtotal, customer_group)
- Order entity must support applied_promotion_ids field (array or JSON list of promotion IDs applied at checkout)
- Price-calculation hook must call promotion engine and apply discounts
- Scheduler must exist to enforce start/end date rules (deactivate expired promotions automatically, or check at runtime)
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 promotion ‘Winter sale’: condition type min_subtotal (value €100), action percent_discount (10%), start Dec 1, end Dec 31, active=true
- Test promotion on cart with €95 subtotal: does NOT apply (below threshold); discount=€0
- Test promotion on cart with €120 subtotal: applies; discount=€12 (10% of €120)
- Create promotion ‘Buy 3, get 1 free’: action type buy_x_get_y (value {buy_qty: 3, get_product_id: ‘shirt’, get_qty: 1})
- Cart has 3 items of product ‘shirt’ (€30 each, total €90); promotion applies; free ‘shirt’ (qty 1, price €0) added; final cart total=€90 (was €120)
- Two promotions active: ‘Winter sale’ (priority 10, 10% off) and ‘Buy 2 get €5 off’ (priority 20, €5 off if qty>2)
- Test with cart that qualifies for both: both promotions apply (no conflict); final discount = 10% + €5 (or per conflict-resolution rule)
- Create promotion code ‘SAVE10’ linked to ‘Winter sale’ promotion, max_uses=100
- Apply code ‘SAVE10’ at checkout; promotion ‘Winter sale’ is automatically applied (code links it)
- Use code 100 times (uses_count=100); 101st attempt to use returns ‘Code limit reached’
- Create promotion code with expiry_date=2026-01-01; attempt to use on 2026-01-02 fails with ‘Code expired’
- Promotion ends at 2026-12-31 23:59:59; attempt to apply on 2027-01-01 00:00:00 fails (time-window check)
- Deactivate promotion; active=false; attempt to apply any codes for that promotion fails
Risks
Promotion condition evaluation can get expensive on large carts (many items, many promotions to check); add memoization of condition results or index promotions by common criteria. Conflicting promotions (e.g., ‘max 1 promotion’ vs. ‘stack all’): define tiebreaker rule explicitly (e.g., higher priority wins, or all equal priority=sum discounts). Test: cart with €100 subtotal, promotion 1 gives €50 off, promotion 2 gives 20% off—should final be €30 (€100 - €50 - 20% of €50) or €30 (€100 - €50 - 20% of €100)? Decide and document. Promo code reuse: if code is not properly linked to promotion_id, merchant could activate wrong code; add validation that code and promotion match before applying. Cart modification after promo applied: customer removes item, discount no longer valid—re-check conditions at payment. Promotion analytics: if order is refunded, should refund amount count against promotion usage/revenue? Decide: track applied discount, or track actually-paid discount (post-refund)?
Commercial context
| Suggested price | EUR 300-500/year or EUR 600 one-off |
| Rival anchor | Amasty Special Promotions |
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.