Checkout & payments
Structured Address Model
Generated from docs/plan/core/structured-address-model/PLAN.md in the AstroBaaS repository. Nothing described below is implemented — it is the written plan for it.
Order.address is a flat text string, breaking invoicing, shipping, and compliance. This core migration refactors addresses into structured fields (street line 1/2, city, postcode, country, company), enabling proper validation and invoicing.
The problem
My invoices are untrackable because billing address is a single text string. I can’t validate postcodes, separate street from apartment number, or export to accounting software. Compliance audits fail because address data is unstructured.
What it does
- Replace Order.address string with structured Address object on Order
- Address fields: line1 (required), line2 (optional), city (required), state/region (optional), postcode (required), country (required), company (optional), email (required), phone (optional)
- Same structure on Customer (for address books and saved addresses)
- Validation: postcode format by country (e.g., US zip code vs. UK postcode)
- Backward compatibility: orders with old string-only address remain readable (soft migration, old records unchanged)
- Billing vs. Shipping: separate Address objects on Order (if different from order-wide address)
- Export: address fields are individually queryable for invoicing/label generation
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.
- Address validation via third-party service (SmartyStreets, Google Maps) — merchants can integrate optionally; core just stores what the user entered.
- Address auto-completion — that’s theme/storefront UX; core just validates the structured submission.
Data model
Migration: replace Order.address (string) with Order.billing_address (Address object). Add Order.shipping_address (optional; if omitted, shipping uses billing). Define Address interface: { line1: string, line2?: string, city: string, state?: string, postcode: string, country: string, company?: string, email: string, phone?: string }. Customer gets the same structure. Schema version bump required (customers reading Orders must handle both old and new formats for a transition period).
API
- POST /api/orders — checkout request now includes billing_address (structured) and optional shipping_address
- GET /api/orders/{id} — response includes billing_address and shipping_address as structured objects
- GET /api/customers/{id} — customer’s saved address is structured
Every route added here must also appear in src/pages/openapi.json.ts — a test
fails the build if it does not.
Admin
Order detail view shows address fields separately: street line 1, line 2, city, postcode, country (editable for certain roles). Invoice generation uses these fields to format a proper address block.
The seam — why this is core
Core owns the Address model and structure. Theme owns the checkout form that collects address fields and submits them as a structured object.
Core interface seam. Without this, invoicing is impossible and compliance breaks. Order.address is currently a string (src/core/models.ts:999); refactoring to structured fields is foundational infrastructure that merchants must never pay for.
Dependencies
- payment-status-tracking — orders must exist before invoices can be issued
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.
- Order created with billing_address: { line1: ‘123 Main St’, line2: ‘Apt 4B’, city: ‘Berlin’, postcode: ‘10115’, country: ‘DE’ }
- Order detail view shows address fields separately (not as one string)
- Generated invoice includes properly formatted address block
- CSV export of orders includes separate address columns (line1, line2, city, postcode, country)
- Old orders (pre-migration) with Order.address as string remain readable; no data loss
- Customer saved address is structured; when used to prefill checkout, form fields populate from structured data
- Postcode validation: ‘ABC’ submitted for ‘DE’ (Germany) → rejected (invalid format); ‘10115’ accepted
Risks
Migration: converting all old address strings to structured fields is a one-time cost. A smart parser can infer structure (e.g., ‘123 Main St, Apt 4B, Berlin’ → line1=‘123 Main St’, line2=‘Apt 4B’, city=‘Berlin’), but accuracy will be ~80%; merchants may need to review and fix manually. Backward compatibility: old records with string-only address need a migration path; reading code must handle both formats. Validation strictness: if postcode validation is too strict, legitimate addresses are rejected; if too loose, typos aren’t caught. A middle ground: warn on suspect postcodes, don’t reject.
Commercial context
| Suggested price | Free |
| Rival anchor | Magento ships full address entities with street lines, city, region, postcode, company, telephone, and VAT ID on both billing and shipping sides. |
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.