dist/codex/spree-commerce/skills/spree-checkout/SKILL.md
Implement Spree's checkout — the Order state machine (cart → address → delivery → payment → confirm → complete), the Payment and Shipment sub-state machines, the return flow (ReturnAuthorization → CustomerReturn → Reimbursement → Refund), guest checkout, payment-step skipping for credit-covered orders, and the V3 checkout API surface. Use when building or customizing checkout flows, debugging state transitions, or wiring custom checkout steps.
npx skillsauth add orcaqubits/agentic-commerce-claude-plugins spree-checkoutInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Fetch live docs:
Spree::Order source on GitHub for the state_machine block — transitions and callbacks change.spree-starter.cart → address → delivery → payment → confirm → complete
↓
skip if store-credit-covered
Each transition validates prerequisites:
| State | Prerequisites |
|-------|---------------|
| cart | One or more line items |
| address | Bill + ship address present |
| delivery | Shipping method selected for every shipment |
| payment | At least one payment method valid for the total |
| confirm | Optional review step (configurable) |
| complete | All sub-states valid; transitions trigger fulfillment + emails |
Order#payment_state is a separate field summarizing all Payment rows:
| Value | Meaning |
|-------|---------|
| balance_due | Outstanding amount remains |
| paid | Fully paid |
| credit_owed | Refund pending |
| failed | All payments failed |
| void | Voided |
Order#shipment_state summarizes all Shipment rows:
| Value | Meaning |
|-------|---------|
| pending | Awaiting payment / stock |
| ready | Ready to ship |
| partial | Some shipped |
| shipped | All shipped |
| backorder | Inventory shortfall |
| canceled | Canceled |
Shipment#state: pending → ready → shipped (+ canceled)Payment#state: checkout → processing → pending → completed (+ failed, void, invalid)ReturnAuthorization (authorized | canceled)
→ CustomerReturn
→ Reimbursement (pending | reimbursed | errored)
→ Refund (against original Payment)
StoreCredit reimbursements skip the Refund step and credit the user's balance.
If order.outstanding_balance.zero? after store-credit/gift-card application, the state machine skips payment and goes straight to confirm. Useful for free-trial / 100%-off scenarios.
Spree supports guest checkout by default — orders carry an email and order_token even without a User. The token allows a guest to revisit their order. Convert guests to users post-checkout via Spree::Order#associate_user!.
Add a custom step by inserting into the state machine via decorator:
# app/models/spree/order_decorator.rb
module MyApp::OrderDecorator
def self.prepended(base)
base.state_machine.before_transition to: :delivery, do: :verify_gift_message
end
def verify_gift_message
# …
end
Spree::Order.prepend(self)
end
Custom steps are powerful but upgrade-fragile — Spree's state machine evolves. Prefer events or service objects when you only need to react.
Headless checkout typically:
POST /api/v3/store/cart — create cart (returns ord_… ID + cart token)POST /api/v3/store/cart/line_items — add itemsPUT /api/v3/store/checkout — set addresses, shipping method, payment methodPOST /api/v3/store/checkout/payment_sessions — create a PaymentSession (Stripe/Adyen/PayPal)POST /api/v3/store/checkout/complete — finalize(Verify exact paths in the v3 API reference — endpoint shapes are still settling.)
The v5.4 PaymentSession abstracts the payment-provider handshake. The storefront creates a PaymentSession, the user authorizes via the gateway's hosted UI (Stripe Elements, Adyen Drop-in, PayPal Checkout), and the session is captured into a Payment on completion. Provider-specific.
order.state # one of cart/address/delivery/payment/confirm/complete
order.payment_state # balance_due/paid/...
order.shipment_state # pending/ready/...
order.can_transition?(:complete) # check before triggering
order.next! # advance to the next state if valid
order.complete! # force to complete if valid (typically last step)
order.cancel! # cancel + revert inventory
Never call update_attribute(:state, …) directly — bypasses callbacks and corrupts inventory/payments.
class CheckoutSubscriber < Spree::Subscriber
subscribes_to 'order.completed'
on 'order.completed', :send_welcome_email
def send_welcome_email(event)
return if event.order.user.nil?
# …
end
end
Events fire after the database commit — safe to enqueue background jobs.
/complete — prevent double-charging on retry.order.errors.full_messages
order.valid?(state) # validate for a specific state
order.checkout_steps # configured step list
ra = Spree::ReturnAuthorization.create!(order: order, return_items: items)
ra.authorize!
cr = Spree::CustomerReturn.create!(return_items: ra.return_items, stock_location: location)
cr.fully_received?
reimbursement = Spree::Reimbursement.create!(customer_return: cr, order: order)
reimbursement.perform! # creates Refunds or StoreCredits
state directly → corrupts inventory and payment totals.Spree::OrderUpdater.completed manually → use the gateway's capture flow; manual completion skips reconciliation.confirm as required — it's configurable (checkout_steps order).Order#state. Re-fetch after every mutating call.state_machine — survives minor upgrades poorly; prefer event subscribers when possible.Always re-verify state names and transition guards against the live Spree::Order source for the version you target.
development
Build with Spree's headless Next.js storefront — the official `spree/storefront` repo (Next.js 16 App Router with Server Actions and Turbopack, React 19 Server Components, Tailwind CSS 4, TypeScript 5, `@spree/sdk`, Sentry), server-only auth (httpOnly JWT cookies + publishable key), MeiliSearch faceted catalog, one-page checkout with Apple/Google Pay/Klarna/Affirm/SEPA, multi-region market routing, GA4 + JSON-LD SEO, and Vercel/Docker deployment. Use when forking or customizing the storefront, or evaluating headless adoption.
tools
Build Spree extensions as Rails engines — gem scaffolding, `bin/rails g spree:extension`, mounting routes/migrations/assets, the modern `prepend` decorator pattern (`*_decorator.rb` with `self.prepended(base)`), generators (`spree:model_decorator`, `spree:controller_decorator`), the four customization surfaces in preference order (Events > Webhooks > Dependencies > Decorators), Spree::Dependencies for swapping service objects, gem release/versioning, and the deprecated Deface engine. Use when building a reusable Spree extension or adding non-trivial customization to an app.
development
Build with Spree's event bus and Webhooks 2.0 — `Spree::Events` publication, `Spree::Subscriber` DSL with `subscribes_to` and `on`, wildcard matching, lifecycle events (`{model}.created/.updated/.deleted` via `publishes_lifecycle_events`), the canonical event catalog (order.*, payment.*, shipment.*, product.*), Webhooks 2.0 endpoints, HMAC-SHA256 signing (`X-Spree-Webhook-Signature`), exponential-backoff retries, and Sidekiq job orchestration. Use when wiring event-driven business logic, building webhook consumers, or replacing ActiveSupport callback chains.
tools
Cross-cutting Spree development patterns — the customization preference hierarchy (Events > Webhooks > Dependencies > Decorators), `Spree::Dependencies` service-object swapping, the `_decorator.rb` + `prepend` + `self.prepended` idiom, idempotent subscribers and webhook receivers, multi-store scoping discipline, prefixed IDs, calculator polymorphism (shipping/promotion/tax share the base), service-object composition with `dry-monads` or simple results, why to avoid `class_eval` reopening and Deface, and Spree-on-Rails idioms (Hotwire/Turbo Stimulus, ActiveStorage, Action Cable, Sidekiq). Use when designing the architecture of a Spree extension or solving cross-cutting concerns.