AstroBaaS

Orders & fulfilment

Shipping Rules Engine

Free — GPL coresize Mplanned, not built

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

A rule-based shipping calculator that accepts conditions (destination region, order weight, order value, product type, postcode) and applies tiered rates. Core provides the rule engine and honest condition evaluator; paid carrier packs plug into this seam.

The problem

Merchants need to charge different shipping rates based on destination region, order weight, value, and product type, but postcode-zone matching alone doesn’t support complex conditions. They’re stuck offering flat rates or building custom code.

What it does

  • Define shipping rules with conditions: destination region (country/postcode), order weight, order value (in minor units), product type/tag
  • Support rule operators: >=, <=, ==, in [list], regex match on postcode
  • Evaluate rule priority: first-matching rule wins (order by priority field)
  • Calculate shipping cost: fixed amount or percentage of order value
  • Apply rule on checkout (before payment)
  • Support free shipping threshold (if order >= X, cost = 0)
  • Exclude specific products from free shipping (e.g., gift cards)
  • Display estimated shipping cost and breakdown in storefront checkout
  • Audit log: log every shipping rule change and calculation
  • Admin UI: create, edit, delete, reorder shipping rules
  • Bulk import rules from CSV
  • Test rule engine: simulate checkout with X weight in Y postcode → shows applicable rate

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.

  • Real-time carrier rates (UPS, DHL API queries) — that’s a paid carrier pack responsibility. Reason: credential management and per-country compliance.
  • Tax calculation on shipping — core shipping does not own tax logic. Reason: tax is a separate subsystem (paid feature).
  • Shipping insurance — out of scope. Reason: paid upsell.
  • Multi-leg shipping (A→B→C) — core assumes single-leg delivery. Reason: complex logistics planning is paid.
  • Delivery date estimation — core does not calculate ETA. Reason: paid feature (delivery-radius-eta-estimation).
  • Regional surcharges by carrier — core rule engine is carrier-agnostic. Reason: carrier packs own surcharge logic.

Data model

New tables: ShippingRule {id, name, priority (int, lower=first), conditions: [{field, operator, value}], costType (‘fixed’|‘percentage’), costValue (integer, minor units or bps), isActive, createdAt, updatedAt}; ShippingRuleAuditLog {id, ruleId, action (‘created’|‘updated’|‘deleted’), oldValue, newValue, timestamp, userId}. No migration.

API

  • POST /api/shipping-rules
  • GET /api/shipping-rules
  • GET /api/shipping-rules/:id
  • PUT /api/shipping-rules/:id
  • DELETE /api/shipping-rules/:id
  • POST /api/shipping-rules/calculate
  • POST /api/shipping-rules/bulk-import
  • POST /api/shipping-rules/reorder

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

Admin

List view: all shipping rules, sortable by priority, editable name/status. Create/Edit form: rule name, conditions builder, cost type and value. Test panel: input weight, postcode, order value → shows which rule applies and cost. CSV export/import buttons. Audit trail for rule changes.

The seam — why this is core

Core owns: rule engine, honest condition evaluator, rule priority logic, cost calculation. Paid owns: carrier integrations that override/append rules (UPS connector returns live rate that takes priority).

GPL-3.0 core, paid modules in a separate repo. Core owns the interface and honest hand-modelled implementation; paid carrier packs plug into this seam without core knowing about individual carriers.

Dependencies

  • Product database (to filter by product tag)
  • Settings system (enable/disable shipping-rules-engine)
  • Order/checkout context (to know weight, value, destination at checkout time)

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.

  • Rule with condition destination_country == ‘GR’ and cost 500 is applied when destinationCountry=‘GR’; all other rules skipped
  • Rules evaluated in priority order; first matching rule wins; if two rules match, lower-priority-number rule is used
  • Postcode regex ^GR-\d{5}$ correctly matches ‘GR-12345’ but not ‘GR-1234’
  • Free shipping threshold: if orderTotal >= 5000 minor units and rule has cost=0, shipping cost is 0
  • Exclude product tag: order with productTag=‘gift_card’ skips free-shipping threshold
  • CSV import with 100 rules completes in <5 seconds; all rules inserted with correct priority
  • Audit log records every rule create/update/delete with oldValue and newValue
  • Calculate endpoint returns {applicableRule: {id, name, priority}, cost: 500, breakdown: […]}
  • If no rule matches, error response {error: ‘no_shipping_rule_matched’} or default free shipping
  • Reorder endpoint updates all rule priorities atomically; if reorder fails, no priorities change

Risks

Logic bomb: rule with no conditions matches all orders (cost=0 free shipping); mitigation: UI prevents empty conditions. Priority collision: two rules with same priority cause non-determinism; mitigation: unique constraint. Regex DOS: malicious postcode regex hangs; mitigation: regex validation, safe engine. Decimal rounding: 333 minor units with 15% = confusion; mitigation: always floor for cost.

Commercial context

Suggested priceFree (core)
Rival anchorShopify Settings (free, built-in), Magento Shipping Rules (€800–1500/year extension), WooCommerce Table Rate Shipping (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.