Checkout & payments
Payment Status Tracking
Generated from docs/plan/core/payment-status-tracking/PLAN.md in the AstroBaaS repository. Nothing described below is implemented — it is the written plan for it.
Merchants can’t see if payment is pending, authorized, or captured, making cash flow forecasting impossible. This core feature tracks payment_status separately from order status, showing merchants real payment state.
The problem
An order shows ‘processing’ but payment never cleared; I shipped goods unpaid. Payment status must be tracked separately from fulfillment status so I know what I actually received.
What it does
- Payment status enum: ‘unpaid’, ‘pending’, ‘paid’, ‘failed’, ‘refunded’
- Distinct from order status (‘pending’, ‘processing’, ‘on-hold’, ‘completed’, ‘cancelled’, ‘refunded’)
- Update via webhook: payment webhook (Stripe, PayPal, etc.) moves payment_status, not order status
- Admin view: show both statuses side-by-side on order detail
- Filtering: /api/orders?payment_status=pending to find all awaiting payment
- Reporting: payment status breakdown (how much is paid, pending, failed) for cash flow forecasting
- Refund tracking: refunded orders show payment_status=‘refunded’; partial refunds keep status=‘paid’ until fully refunded
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 order cancellation if payment fails — that decision belongs to merchant policy, not core. Core records the failure; theme/merchant decides what to do.
- Automatic retry of failed payments — theme can offer a ‘retry payment’ button; core just tracks status.
Data model
Add to Order: payment_status (enum: ‘unpaid’ | ‘pending’ | ‘paid’ | ‘failed’ | ‘refunded’). Change existing status field description to clarify it is order/fulfilment status, not payment status. Both fields on every order.
API
- GET /api/orders?payment_status=paid — filter by payment status
- GET /api/orders/{id} — response includes payment_status
- POST /api/payments/webhook/{provider} — update payment_status based on provider event
- GET /api/reports/payments — cash flow report: { unpaid, pending, paid, failed, total_cents }
Every route added here must also appear in src/pages/openapi.json.ts — a test
fails the build if it does not.
Admin
Order list view shows two status badges: Order Status (e.g., ‘Processing’) and Payment Status (e.g., ‘Pending’). Order detail view shows both prominently. Payment section of order detail shows payment method, provider, reference id, and status. Refund history shows refund_status for each refund.
The seam — why this is core
Core owns the payment_status enum, validation, and webhook routing. Providers (Stripe, PayPal, etc.) own the specific event types they send. The separation ensures payment state is independent of order state.
Core owns the interface + honest payment state viewer; payment visibility is infrastructure, not a per-country obligation or credential.
Dependencies
- shopping-cart → payment placement
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: payment_status=‘unpaid’, order status=‘pending’
- Stripe webhook (charge.succeeded): payment_status=‘paid’, order status changes to ‘processing’ (if merchant configured auto-transition)
- Order view shows Order Status=‘Processing’, Payment Status=‘Paid’
- Partial refund of €50: payment_status stays ‘paid’; refunds array shows one entry, total_refunded=€50
- Full refund (€150): payment_status=‘refunded’
- Filter /api/orders?payment_status=pending returns all orders awaiting payment confirmation
- Report shows: €5,000 paid, €1,200 pending, €300 failed (totals merchant’s current cash position)
Risks
Webhook race: payment webhook arrives after order is already marked paid — idempotency must prevent double-marking. Payment webhook arrives out of order (REFUND before PAYMENT) — state machine must handle this gracefully (refund on an already-paid order is OK, refund on unpaid order is suspicious). Merchants may conflate the two statuses — admin help text and reporting must clarify the difference consistently.
Commercial context
| Suggested price | free (core) |
| Rival anchor | Shopify: included; Magento: included |
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.