Pricing & promotions
Price History (Omnibus)
Generated from docs/plan/core/price-history/PLAN.md in the AstroBaaS repository. Nothing described below is implemented — it is the written plan for it.
Merged from duplicate proposals: “Price History (Omnibus)”
Tiering note. STORING price history is core — one table, one write on save. The maintained per-country Omnibus display and audit pack is the paid half. A merchant must never be unable to comply with the law because they did not pay.
Tracks the lowest price a product has held over the last 30 days and displays it prominently during promotions, satisfying EU Omnibus Directive 2024 compliance. Every price change is logged with timestamp; admin UI shows historical minimums; storefront displays comparison (original €50 → now €35, lowest €32 in last 30 days).
The problem
EU merchants face €10,000+ fines if they claim ‘40% off!’ but can’t prove the product was at the higher price for 30 of the last 30 days. Currently, there is no audit trail of prices, so merchants either manually track prices in a spreadsheet or face regulator liability.
What it does
- Log every price change (productVariant.price or product.listPrice) with timestamp and previous price to price_history table
- Calculate and cache minimum price over last 30 calendar days (updated daily)
- Expose min_price_30d and min_price_30d_date in GET /products/:id/variants/:variantId response
- Storefront component displays: ‘Was €50 (lowest €32 in 30 days), now €35’
- Admin product editor shows historical price graph (last 30 days min/avg/current as line chart)
- Audit log includes price changes with reason (e.g., ‘manual edit’, ‘promotion_active’, ‘competitor_match’)
- Export price history as CSV for regulator submissions (GET /admin/audit/price-history?format=csv)
- Price history data is immutable once recorded (no edits, only append)
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.
- Automatic price decreases based on competitor prices (see dynamic-pricing-by-season for algorithmic pricing)
- Per-customer price history (price-history is per-product; see loyalty-points for customer-tier pricing)
- Prices older than 30 days are soft-deleted from storefront display but kept in audit forever (not a feature, an implementation detail)
Data model
Add price_history table: { id, productVariantId, old_price, new_price, changed_at, reason, changed_by_user_id }. Add computed field productVariant.min_price_30d (INT, recomputed daily). No migration: new shops start tracking immediately; existing shops receive current price as first history entry.
API
- GET /products/:id/variants/:variantId — include price_history_30d array with { changed_at, price } for graphing
- GET /admin/audit/price-history?productVariantId=X&start=2024-08-01&end=2024-09-03 — paginated history
- GET /admin/audit/price-history?format=csv — export for regulator submission
Every route added here must also appear in src/pages/openapi.json.ts — a test
fails the build if it does not.
Admin
In product variant editor, add ‘Price History’ section below price input. Show a 30-day line graph (min/avg/current as different colors). Below: table of last 10 price changes (date, old→new, reason). Button: ‘Download history for regulator’. When creating a promotion, require a text field ‘Original price justification’ (stores as reason in audit log).
The seam — why this is core
Core owns price history structure and 30-day minimum calculation. This is EU compliance infrastructure, not a merchant workflow feature. No paid upsell: every merchant using core pricing must comply with Omnibus Directive.
core owns the interface + an honest hand-modelled implementation — price history storage and 30-day min/max display logic is straightforward data management and integer arithmetic.
Dependencies
- Audit log system (to log price changes)
- Product pricing schema (productVariant.price must already exist)
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.
- When a product price changes from €50 to €35 at 14:30 UTC on Sep 3, price_history records {old_price: 5000, new_price: 3500, changed_at: 2024-09-03T14:30:00Z}
- min_price_30d is €32 if the lowest price in the 30 calendar days prior was €32
- If a price was €50 for 29 days and €32 for 1 day, then increased to €35, the claim ‘40% off’ is NOT compliant (lowest was only 30 days ago, not the original €50)
- Storefront displays ‘lowest price in 30 days: €32’ even after the promotion ends
- Exporting price history as CSV includes columns: product_id, variant_id, changed_at, old_price, new_price, changed_by_user, reason
- Price history is not editable; attempting to update a past entry via API returns 403 Forbidden
Risks
If min_price_30d is computed at query time instead of cached daily, storefront queries become expensive (full table scan per product). If price_history is pruned after 30 days, merchants lose proof of compliance for audits (always keep forever). If reason field is not populated, merchants cannot prove to regulators why the promotion is legitimate.
Commercial context
| Suggested price | Free (core) |
| Rival anchor | Shopify (free, built-in); Magento (free, built-in); WooCommerce (free plugin) |
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.