AstroBaaS

Pricing & promotions

Multi-Currency Pricing Engine

Free — GPL coresize Lplanned, not built

Generated from docs/plan/core/multi-currency-pricing-engine/PLAN.md in the AstroBaaS repository. Nothing described below is implemented — it is the written plan for it.

Merchants expand into new markets by displaying prices in local currencies without manual conversion or external integrations. The system stores a primary currency per price, maintains fallback logic for unsupported currencies, and surfaces currency selection UI in storefronts; pricing math remains integers throughout.

The problem

A UK merchant selling into France and Germany sees EUR-only prices on their storefront, forcing French customers to do mental math. Conversion drop is 40%+ per currency tier when prices aren’t shown in local money.

What it does

  • Store currency as a field on every productVariant.price (ISO 4217 code, e.g. ‘GBP’, ‘EUR’, ‘CHF’)
  • Display prices in storefront matching customer’s detected location/preference (via storefront SDK or explicit choice)
  • Define fallback chain: preferred currency → merchant default → primary currency
  • Return prices by currency in REST list endpoints (GET /products returns prices per currency variant)
  • Localize price display component UI in admin (no merchant sees ‘EUR’ if their market is GBP-only)
  • Store tax rules per-currency (VAT 19% in DE, 20% in UK as separate rules, not conversion)
  • Audit log tracks price change with old/new currency and amount
  • Storefront component renders stripped prices (e.g. €50.00 not EUR 5000) and currency symbol matching locale

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.

  • Live exchange rate fetching (see live-exchange-rates paid module; core does not pull external data)
  • Automatic currency detection from payment method (storefront owns this decision via browser Intl API or user choice)
  • Currency-specific formatting rules beyond locale (£1,234.56 vs 1.234,56€ — storefront owns via Intl.NumberFormat)
  • Conversion history or audit of rate changes (paid module owns rate tracking)
  • Per-shipping-zone currency rules (too much state; merchants define zones once, prices are global fallback-aware)

Data model

Add currency: string (ISO 4217) to productVariant.price table; default = shop.currency setting. Add prices: { [currency]: number } to productVariant for multi-currency storage (schema migration if libSQL or relational). Existing shops default all prices to their shop.currency; new shops can opt-in via settings.multi_currency_enabled.

API

  • GET /products?include=prices_by_currency — returns variant prices keyed by ISO code
  • GET /products/:id?currencies=EUR,GBP,CHF — return prices only in requested currencies
  • PUT /admin/products/:id/variants/:variantId/prices — upsert price for each currency
  • GET /admin/settings/currency — read shop’s primary currency and supported currencies list

Every route added here must also appear in src/pages/openapi.json.ts — a test fails the build if it does not.

Admin

Add a ‘Multi-Currency Setup’ card in Settings > Shop > Pricing. Checkboxes for supported currencies; dropdown for primary. When enabled, product variant editor shows price inputs for each enabled currency. Show currency badge per price in variant table. Add column header toggle to hide/show currency-specific prices.

The seam — why this is core

Core owns the multi-currency price data structure (currency field, fallback logic, and integer math). Merchants can maintain a static list of currencies they support. Paid live-exchange-rates module owns the scheduler that pulls daily rates and the guarantee of freshness — core cannot own an external dependency or SLA.

core owns the interface + an honest hand-modelled implementation — the data structure (currency field per price, integer arithmetic), display routing, and fallback logic are foundational. Merchants can self-maintain static rates or integrate external sources.

Dependencies

  • product-customization-options (if merchants sell options with per-currency upcharges)
  • Existing product schema (variants table must support currency field)

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.

  • A UK shop with GBP primary can add EUR price to a variant; both currencies store independently without conversion
  • A variant with no EUR price falls back to GBP when customer requests EUR (fallback logic is tested)
  • Storefront SDK receives prices as { [currency]: integer } and displays per-locale rules without server re-rendering
  • An order placed in EUR records order.currency=‘EUR’ and order.items[].pricePerUnit in minor units of EUR, not converted
  • Audit log shows price change from EUR 2500 to EUR 3000 as separate from GBP change, not merged
  • Deleting a currency from shop settings does not delete stored prices, only hides them from storefront selection
  • Tax calculation uses the order’s currency, not shop currency (€50 at 19% VAT = €9.50, not EUR 950)

Risks

If schema migration is done carelessly (libSQL/relational), existing variants lose price data; always ADD new currency column, never REPLACE. If fallback logic inverts (currency → shop currency → primary), wrong prices display. If storefront SDK receives prices as strings or floats, integer arithmetic breaks in checkout. If payment processors reject ambiguous currency codes (use ISO 4217 only).

Commercial context

Suggested priceFree (core)
Rival anchorShopify Markets (£23/mo ~€27/mo); Magento (free framework)

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.