@spree/docs 0.1.239 → 0.1.241

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/api-reference/store-api/metadata.md +6 -6
  2. package/dist/developer/core-concepts/architecture.md +15 -19
  3. package/dist/developer/core-concepts/carts.md +71 -1
  4. package/dist/developer/core-concepts/metafields.md +0 -2
  5. package/dist/developer/create-spree-app/quickstart.md +22 -19
  6. package/dist/developer/customization/api.md +2 -2
  7. package/dist/developer/customization/checkout.md +117 -0
  8. package/dist/developer/customization/configuration.md +76 -175
  9. package/dist/developer/customization/decorators.md +120 -130
  10. package/dist/developer/customization/metadata.md +9 -9
  11. package/dist/developer/customization/model-preferences.md +34 -68
  12. package/dist/developer/customization/permissions.md +13 -28
  13. package/dist/developer/customization/validations.md +16 -43
  14. package/dist/developer/dashboard/concepts.md +4 -2
  15. package/dist/developer/dashboard/customization/backend.md +5 -2
  16. package/dist/developer/dashboard/customization/navigation.md +1 -1
  17. package/dist/developer/dashboard/customization/quickstart.md +1 -1
  18. package/dist/developer/dashboard/overview.md +2 -2
  19. package/dist/developer/dashboard/plugins/distributing.md +1 -1
  20. package/dist/developer/dashboard/public-api.md +3 -2
  21. package/dist/developer/dashboard/recipes/attribute-end-to-end.md +3 -3
  22. package/dist/developer/dashboard/recipes/custom-form-field.md +1 -1
  23. package/dist/developer/dashboard/recipes/page-action-button.md +2 -2
  24. package/dist/developer/dashboard/recipes/sidebar-widget.md +2 -2
  25. package/dist/developer/dashboard/slots-catalog.md +60 -0
  26. package/dist/developer/getting-started/quickstart.md +25 -24
  27. package/dist/developer/how-to/custom-promotion.md +1 -1
  28. package/dist/developer/how-to/custom-search-provider.md +2 -2
  29. package/dist/developer/how-to/custom-stock-splitter.md +2 -2
  30. package/dist/developer/sdk/admin/extending.md +1 -1
  31. package/dist/developer/sdk/admin/quickstart.md +1 -1
  32. package/dist/developer/sdk/admin/resources.md +1 -1
  33. package/dist/developer/sdk/configuration.md +1 -1
  34. package/dist/developer/sdk/extending.md +1 -1
  35. package/dist/developer/tutorial/testing.md +45 -91
  36. package/package.json +1 -1
@@ -6,7 +6,7 @@ description: "How to store and retrieve custom metadata on carts, orders, and li
6
6
 
7
7
  The Store API supports storing arbitrary key-value **metadata** on carts (orders) and line items. Metadata is useful for tracking custom information like gift messages, attribution data, or integration-specific fields.
8
8
 
9
- > **NOTE:** Metadata is different from [metafields](../../developer/core-concepts/metafields.md). Metadata is simple JSON storage (untyped, no schema), while metafields are structured, typed, and schema-defined. Use metadata for integration data and internal tracking. Use metafields for customer-facing custom attributes.
9
+ > **NOTE:** Metadata is different from [custom fields](../../developer/core-concepts/metafields.md). Metadata is simple JSON storage (untyped, no schema), while custom fields are structured, typed, and schema-defined. Use metadata for integration data and internal tracking. Use custom fields for customer-facing custom attributes.
10
10
 
11
11
  ## Cart Metadata
12
12
 
@@ -146,18 +146,18 @@ Metadata is stored as a flat JSON object. You can use any keys and values:
146
146
  }
147
147
  ```
148
148
 
149
- ## Metadata vs Metafields
149
+ ## Metadata vs Custom Fields
150
150
 
151
- Spree has two permanent, complementary systems for custom data — **metadata for machines, metafields for humans**.
151
+ Spree has two permanent, complementary systems for custom data — **metadata for machines, custom fields for humans**.
152
152
 
153
- | | Metadata | Metafields |
153
+ | | Metadata | Custom Fields |
154
154
  |-|----------|------------|
155
155
  | **Purpose** | Developer escape hatch — integration IDs, sync state | Merchant-defined structured attributes |
156
- | **Schema** | Schemaless JSON — no definition required | Defined via MetafieldDefinitions (typed, validated) |
156
+ | **Schema** | Schemaless JSON — no definition required | Defined via CustomFieldDefinitions (typed, validated) |
157
157
  | **Validation** | None | Type-specific validation |
158
158
  | **Visibility** | Write-only in Store API, readable in Admin API | Configurable (admin-only or public) |
159
159
  | **Admin UI** | JSON preview | Dedicated form fields |
160
160
  | **Best for** | Integration data, tracking, attribution, write-and-forget | Product specs, custom attributes, customer-facing data |
161
161
  | **API access** | Write via Store API, read via Admin API | Read/write via both APIs |
162
162
 
163
- Both systems are here to stay. For more details on metafields, see the [Metafields guide](../../developer/core-concepts/metafields.md).
163
+ Both systems are here to stay. For more details on custom fields, see the [Custom Fields guide](../../developer/core-concepts/metafields.md).
@@ -5,9 +5,10 @@ description: How Spree fits together — the catalog, the cart, the order, and f
5
5
 
6
6
  ## Overview
7
7
 
8
- Spree is a commerce engine you drive through a REST API. It holds the catalog, works out what a customer owes, takes the money, and tracks what was sent. You build the storefront, the mobile app, or the internal tool on top of it.
8
+ Spree is a commerce engine you drive through a REST API. It holds the catalog, works out what a customer owes, takes the money, and tracks what was delivered. You build the storefront, the mobile app, Point of Sale or an internal tool on top of it.
9
9
 
10
- There is no built-in storefront you have to accept. Everything a customer-facing app needs is on the Store API, and everything a back office needs is on the Admin API. Both ship as typed TypeScript clients.
10
+ There is no built-in storefront you have to accept, however we do provide a [reference one built in Next.js](../storefront/nextjs/quickstart.md).
11
+ Everything a customer-facing app needs is on the Store API, and everything a back office needs is on the Admin API. Both ship as fully typed TypeScript clients.
11
12
 
12
13
  ## The shape of a purchase
13
14
 
@@ -72,7 +73,7 @@ Three things in that diagram are worth calling out, because they're where Spree
72
73
 
73
74
  **Delivery is described by a profile, not a category.** A [delivery profile](fulfillments.md) says how a product travels — physically shipped, or digital — and which locations and methods serve it.
74
75
 
75
- **Stock lives per location.** A variant has a stock level at each [stock location](inventory.md), so availability is a real question about real warehouses rather than a single number.
76
+ **Stock lives per location.** A variant can have a stock level at each [stock location](inventory.md), so availability is a real question about real warehouses rather than a single number.
76
77
 
77
78
  ## The two APIs
78
79
 
@@ -103,6 +104,7 @@ Spree assumes you'll extend it, and gives you three ways in that survive an upgr
103
104
 
104
105
  | Approach | Use it for | Guide |
105
106
  |---|---|---|
107
+ | **Events** | Reacting inside your own app when something happens — indexing a record, calling an API with your credentials | [Events](events.md) |
106
108
  | **Webhooks** | Telling another system something happened — an ERP, a warehouse, an email tool | [Webhooks](webhooks.md) |
107
109
  | **Custom fields** | Storing your own data on a product, order or customer without changing the schema | [Custom Fields](metafields.md) |
108
110
  | **Providers** | Plugging in a real service for tax, delivery rates, payments or search | [Providers](../providers/overview.md) |
@@ -116,32 +118,26 @@ One Spree installation can run more than one business:
116
118
  - **[Stores](stores.md)** are separate businesses — their own catalog, orders, customers and settings. Data does not cross between them.
117
119
  - **[Channels](channels.md)** are the ways one store sells: a website, a mobile app, a retail till.
118
120
  - **[Markets](markets.md)** are the regions a store sells into, each with its currency, locale and tax treatment.
119
- - **[Sellers](products.md#seller-submissions)** let one store list goods from many vendors, for a marketplace.
121
+ - **[Sellers](sellers.md)** let one store list goods from many vendors, for a marketplace.
120
122
 
121
123
  ## What you install
122
124
 
123
- | Package | What it is |
124
- |---|---|
125
- | `spree` | The engine — catalog, checkout, payments, both APIs |
126
- | `spree_emails` | Transactional email, if you don't send your own |
127
- | `spree_dashboard` | The back-office dashboard |
128
-
129
- Providers for search, delivery rates, tax and payments install separately, so you only carry what you use.
130
-
131
- **TypeScript:**
132
-
133
125
  | Package | What it is |
134
126
  |---|---|
135
127
  | [`@spree/sdk`](../sdk/quickstart.md) | Store API client — for your storefront |
136
128
  | [`@spree/admin-sdk`](../sdk/admin/quickstart.md) | Admin API client — for back-office and integrations |
137
129
  | [`@spree/cli`](../cli/quickstart.md) | Command line for local setup and calling the Admin API |
130
+ | [`@spree/dashboard`](../dashboard/overview.md) | The admin interface, a React app you own and extend |
138
131
 
139
- > **INFO:** Spree runs on PostgreSQL, MySQL or SQLite. PostgreSQL is recommended for production — [database configuration](../deployment/database.md).
132
+ > **INFO:** Spree runs on PostgreSQL, MySQL or SQLite. Check [database configuration](../deployment/database.md) for more details.
140
133
 
141
134
  ## Where to go next
142
135
 
136
+ Two ways in, depending on how you prefer to learn.
137
+
138
+
139
+ - [Build something](../tutorial/introduction.md) — Add one feature end to end — a model with its own API, a screen in the dashboard, a page on the storefront, and the tests around it. Start here if you learn by doing.
140
+ - [Read the reference](products.md) — Every part of the platform, one page each: products and pricing, carts and orders, payments, fulfillment, promotions. Start here if you already know what you need.
141
+
143
142
 
144
- - [Quickstart](../getting-started/quickstart.md) — Get an installation running.
145
- - [Store SDK](../sdk/quickstart.md) — Build a storefront against the Store API.
146
- - [Products](products.md) — How the catalog is modelled.
147
- - [Carts](carts.md) — Cart, checkout, and what completion guarantees.
143
+ Building a storefront rather than extending the backend? Go straight to the [Spree Next.js Storefront](../storefront/nextjs/quickstart.md).
@@ -131,10 +131,80 @@ A cart tells you what it still needs before it can be completed:
131
131
  }
132
132
  ```
133
133
 
134
- An empty `requirements` array means the cart is ready.
134
+ Every entry has the same four fields. Branch on `code` — `message` is translated prose
135
+ meant for a person, and `step` is only a grouping hint.
135
136
 
136
137
  This is what lets you design your own checkout. Spree tells you what's missing; you decide how and when to ask for it — one long page, a few steps, or whatever suits your storefront.
137
138
 
139
+ Seven checks run on every cart read:
140
+
141
+ | `code` | `step` | Raised when |
142
+ |---|---|---|
143
+ | `line_items_required` | `cart` | The cart is empty |
144
+ | `email_required` | `address` | No email address |
145
+ | `ship_address_required` | `address` | Something needs shipping and there is no address |
146
+ | `delivery_method_required` | `delivery` | A delivery method hasn't been chosen |
147
+ | `payment_required` | `payment` | The total isn't covered by payments |
148
+ | `po_number_required` | `address` | A company requires a purchase-order number |
149
+ | `order_minimum_not_met` | `cart` | Below the order minimum, with the shortfall in the message |
150
+
151
+ > **WARNING:** **An empty `requirements` array does not guarantee completion will succeed.** What a
152
+ > cart reports is the *advisory* set. Completing one additionally checks stock
153
+ > (`out_of_stock`, `discontinued`), negotiated quantity rules
154
+ > (`quantity_rule_violated`) and the guest-checkout policy
155
+ > (`guest_checkout_not_allowed`) — each of which has to load line items, so they are
156
+ > held back to keep reading a cart cheap. Treat a failed completion as a normal path,
157
+ > not an exception.
158
+
159
+ ### Checkout steps
160
+
161
+ A cart also reports a step list, so you can render a progress indicator:
162
+
163
+
164
+ ```typescript Store SDK
165
+ const cart = await client.carts.get(cartId)
166
+
167
+ cart.current_step // "delivery"
168
+ cart.completed_steps // ["address"]
169
+ ```
170
+
171
+ ```json Response
172
+ {
173
+ "current_step": "delivery",
174
+ "completed_steps": ["address"]
175
+ }
176
+ ```
177
+
178
+
179
+ Spree has no checkout state machine. The list is computed from the cart's own data,
180
+ and nothing on the server refuses a write because of where the customer appears to be
181
+ — you can set an address, choose delivery and attach a payment in any order.
182
+ `current_step` is simply the step of the first unmet requirement.
183
+
184
+ Five steps are built in, and three only appear when they apply:
185
+
186
+ | Step | Present when |
187
+ |---|---|
188
+ | `address` | Always |
189
+ | `delivery` | The cart has items and isn't entirely digital |
190
+ | `payment` | The total is above zero |
191
+ | `confirm` | A payment method asks for one, or the store always wants one |
192
+ | `complete` | Always |
193
+
194
+ So a free digital order reports `address` then `complete`. A storefront that hardcodes
195
+ five steps will show two that never apply.
196
+
197
+ > **NOTE:** Requirements use one grouping name that is not a step: **`cart`**. Missing line
198
+ > items, the order minimum and stock problems are filed under it, and there is no
199
+ > `cart` entry in the step list — `current_step` reports those as `address`. If you
200
+ > group requirements by `step` to place them on screens, give `cart` a home.
201
+
202
+ Checkout introspection lives on the cart, not the order. An order has already been
203
+ placed, so `current_step` and `requirements` do not appear on it.
204
+
205
+ To add your own step or requirement, see
206
+ [Customizing checkout](../customization/checkout.md).
207
+
138
208
  ## Checkout
139
209
 
140
210
  **Step 1: Add the customer's details**
@@ -9,8 +9,6 @@ Sooner or later you need to store something Spree doesn't have a field for. A fa
9
9
 
10
10
  Custom fields are how you do that without changing the database. You declare a field once — its name, its type, who can see it — and from then on it can be set on any record of that kind, edited in the dashboard, returned by the API, and searched or filtered like any built-in field.
11
11
 
12
- > **NOTE:** Custom fields are also known as **metafields** in the dashboard. The API and SDK call them custom fields throughout.
13
-
14
12
  ## Definitions and values
15
13
 
16
14
  There are two halves, and it helps to keep them straight:
@@ -18,7 +18,7 @@ The CLI walks you through an interactive setup:
18
18
 
19
19
  Once complete, your store is running at [http://localhost:3000](http://localhost:3000) — setup pulls the latest Spree image, seeds the database, and configures API keys, then prints a summary with your admin credentials and keys. If you skipped starting services, the first `pnpm dev` completes setup automatically.
20
20
 
21
- The **React Dashboard** (Developer Preview of the next-generation admin) is available with the `--react-dashboard` flag, or later via `spree add dashboard`.
21
+ The **dashboard** and the marketplace **seller panel** are included in every project — there is no flag to add them.
22
22
 
23
23
  ## Prerequisites
24
24
 
@@ -35,7 +35,7 @@ npx create-spree-app@latest my-store --no-storefront --no-sample-data --no-start
35
35
 
36
36
  | Flag | Description |
37
37
  |------|-------------|
38
- | `--react-dashboard` | Include the React Dashboard (Developer Preview — also available later via `spree add dashboard`) |
38
+ | `--react-dashboard` | No-op — the dashboard is always included. Kept so existing scripts keep working |
39
39
  | `--no-storefront` | Skip Next.js storefront setup |
40
40
  | `--no-sample-data` | Skip loading sample products and categories |
41
41
  | `--no-start` | Don't start Docker services after scaffolding |
@@ -52,29 +52,32 @@ npx create-spree-app@latest my-store --no-storefront --no-sample-data --no-start
52
52
 
53
53
  ```text
54
54
  my-store/
55
+ ├── server/ # The Spree API — a full Rails app from spree/spree-starter
56
+ │ ├── app/ # Your models, controllers, serializers and jobs
57
+ │ ├── config/
58
+ │ ├── Dockerfile # Also builds the production image (API + dashboard)
59
+ │ └── Gemfile
60
+ ├── apps/
61
+ │ ├── dashboard/ # Admin dashboard (React)
62
+ │ │ └── .env.local # Dev proxy target — no credentials
63
+ │ ├── seller-dashboard/ # Seller panel, for marketplaces (React)
64
+ │ └── storefront/ # Next.js storefront (unless --no-storefront)
65
+ │ └── .env.local # API URL + publishable key
66
+ ├── .env # SECRET_KEY_BASE, SPREE_PORT, SPREE_VERSION_TAG, SPREE_SAMPLE_DATA
67
+ ├── .spree/ # CLI credentials — gitignored, created on first run
55
68
  ├── docker-compose.yml # Spree backend (prebuilt image) + Postgres + Meilisearch
56
69
  ├── docker-compose.dev.yml # Alternative: build from local server/
57
70
  ├── render.yaml # Render Blueprint — one Docker service built from this repo
58
- ├── .env # SECRET_KEY_BASE, SPREE_PORT, SPREE_VERSION_TAG, SPREE_SAMPLE_DATA
59
71
  ├── .dockerignore # Keeps the production build context to sources
60
72
  ├── .gitignore
61
- ├── package.json # Convenience scripts
62
- ├── README.md
63
- ├── server/ # Full Rails app — the Spree API (from spree/spree-starter)
64
- │ ├── Gemfile
65
- │ ├── Dockerfile # Also builds the production image (API + dashboard)
66
- │ ├── config/
67
- │ ├── app/
68
- │ └── ...
69
- └── apps/
70
- ├── storefront/ # Next.js storefront (unless --no-storefront)
71
- │ ├── .env.local # API URL + API key
72
- │ └── ...
73
- └── dashboard/ # React Dashboard (with --react-dashboard)
74
- ├── .env.local # Dev proxy target — no credentials
75
- └── ...
73
+ ├── package.json # Scripts and the pinned @spree/cli
74
+ ├── AGENTS.md # Instructions for coding agents
75
+ ├── CLAUDE.md # Project layout and conventions
76
+ └── README.md # How to run everything
76
77
  ```
77
78
 
79
+ **`server/` is the only part that is Spree's.** The apps under `apps/` are ordinary Vite and Next.js projects you own — restyle, restructure or replace them without forking anything. Each carries its own instructions (`server/CLAUDE.md`, `apps/dashboard/AGENTS.md`), which is also what a coding agent reads.
80
+
78
81
  ### What's in docker-compose.yml
79
82
 
80
83
  - **Spree** — one `web` container running the `ghcr.io/spree/spree:latest` image on the configured port (default `3000`); background jobs run in-process via Solid Queue (stored in Postgres — job dashboard at `/jobs`)
@@ -127,7 +130,7 @@ Open [http://localhost:3000/admin](http://localhost:3000/admin) and log in with:
127
130
  | **Email** | `spree@example.com` |
128
131
  | **Password** | `spree123` |
129
132
 
130
- With `--react-dashboard`, the React Dashboard's dev server is the admin instead: `spree dev` starts it alongside the API at [http://localhost:5173](http://localhost:5173) — same credentials, live-reloading from `apps/dashboard/` (the classic admin remains at `/admin`). See the [React Dashboard docs](../dashboard/overview.md).
133
+ The dashboard runs as its own dev server: `spree dev` starts it alongside the API at [http://localhost:5173](http://localhost:5173), with the same credentials, live-reloading from `apps/dashboard/`. See the [dashboard docs](../dashboard/overview.md).
131
134
 
132
135
  ### Store API
133
136
 
@@ -5,7 +5,7 @@ description: Add new Store API endpoints, customize existing JSON responses with
5
5
 
6
6
  Before you start customizing Spree API endpoints, make sure you reviewed all existing API endpoints in the [Spree API docs](../../api-reference/admin-api/endpoints.md).
7
7
 
8
- For a step-by-step walkthrough of adding a complete new resource (model, serializer, controller, routes), see the [API tutorial](../tutorial/admin-api.md).
8
+ For a step-by-step walkthrough of adding a complete new resource (model, serializer, controller, routes), see the [API tutorial](../tutorial/model-and-api.md).
9
9
 
10
10
  ## Customizing JSON Responses
11
11
 
@@ -35,7 +35,7 @@ Restart the server and the Product API will include your new attribute.
35
35
 
36
36
  ### Adding an Association
37
37
 
38
- Let's say you've created a `Spree::Brand` model that belongs to `Product` (see the [tutorial](../tutorial/admin-api.md) for the full example).
38
+ Let's say you've created a `Spree::Brand` model that belongs to `Product` (see the [tutorial](../tutorial/model-and-api.md) for the full example).
39
39
 
40
40
  Create a serializer for the new model:
41
41
 
@@ -0,0 +1,117 @@
1
+ ---
2
+ title: Customizing Checkout
3
+ description: Add your own checkout step or requirement, and reorder or remove the built-in ones.
4
+ ---
5
+
6
+ Spree does not run your checkout — your storefront does. What Spree owns is the list
7
+ of things a cart still needs, which it reports on cart reads and enforces when you
8
+ complete one. Adding to that list is how you make checkout require something of your
9
+ own.
10
+
11
+ > **NOTE:** For what a cart reports and how to read it, see
12
+ > [Carts](../core-concepts/carts.md#checkout-requirements). This page is about
13
+ > adding to it.
14
+
15
+ ## Requirements are the gate, steps are a label
16
+
17
+ Two things are easy to confuse:
18
+
19
+ - A **requirement** is enforced. It appears on cart reads while unmet, and completing
20
+ a cart recomputes the list and refuses if anything is outstanding.
21
+ - A **step** is descriptive. It exists so a storefront can render a progress
22
+ indicator. Nothing on the server checks which step a customer is on.
23
+
24
+ So a requirement is what you reach for when something must be true before an order
25
+ exists. A step is only worth adding when your checkout genuinely has a stage the
26
+ built-in five don't describe.
27
+
28
+ ## Adding a requirement
29
+
30
+ When the flow is unchanged and you only need one more condition:
31
+
32
+ ```ruby server/config/initializers/spree.rb
33
+ Spree::Checkout::Registry.add_requirement(
34
+ step: :address,
35
+ field: :vat_number,
36
+ message: 'VAT number is required for business orders',
37
+ satisfied: ->(cart) { cart.custom_fields['vat_number'].present? }
38
+ )
39
+ ```
40
+
41
+ While `satisfied:` returns false the cart reports the requirement and refuses to
42
+ complete. Its `code` is derived as `<field>_required` — `vat_number_required` here.
43
+
44
+ Pass `applicable:` to scope it. It is checked before `satisfied:`, so a requirement
45
+ that doesn't apply never appears at all:
46
+
47
+ ```ruby
48
+ applicable: ->(cart) { cart.customer&.company.present? }
49
+ ```
50
+
51
+ `message` is stored verbatim — wrap it in `Spree.t` yourself if it needs translating.
52
+
53
+ ## Adding a step
54
+
55
+ A step bundles its own requirements and says when it is satisfied:
56
+
57
+ ```ruby server/config/initializers/spree.rb
58
+ Spree::Checkout::Registry.register_step(
59
+ name: :loyalty,
60
+ before: :payment,
61
+ satisfied: ->(cart) { cart.custom_fields['loyalty_number'].present? },
62
+ requirements: ->(cart) {
63
+ [{ step: 'loyalty', field: 'loyalty_number',
64
+ message: 'Loyalty number is required' }]
65
+ }
66
+ )
67
+ ```
68
+
69
+ The step now shows up in the cart's `current_step` and `completed_steps`, its
70
+ requirements appear in `requirements` while unsatisfied, and completion refuses the
71
+ cart until they are met.
72
+
73
+ Each lambda receives the cart. Placement is controlled by two keywords:
74
+
75
+ | Keyword | Effect |
76
+ |---|---|
77
+ | `before:` / `after:` | Place the step next to an existing one. `before:` wins if both are given |
78
+ | `applicable:` | Whether the step exists for this cart at all — checked before `satisfied:` |
79
+
80
+ An unanchored step lands immediately before `complete`. An anchor naming a step this
81
+ particular cart doesn't have — `before: :payment` on a free cart — falls back to the
82
+ same place.
83
+
84
+ Unlike an added requirement, a step's `requirements:` lambda may set its own `code:`.
85
+
86
+ ## Reordering or removing built-in steps
87
+
88
+ ```ruby server/config/initializers/spree.rb
89
+ # Drop a step from the reported list
90
+ Spree::Checkout::Registry.base_steps.delete('confirm')
91
+
92
+ # Or change when one applies
93
+ Spree::Checkout::Registry.base_steps['payment'] = ->(cart) { cart.total > 0 }
94
+
95
+ # Or reorder wholesale — known names keep their own applicability
96
+ Spree::Checkout::Registry.base_step_names = %w[address payment delivery complete]
97
+ ```
98
+
99
+ > **WARNING:** Removing a step removes it from the reported list. It does **not** remove the
100
+ > requirements filed under it — dropping `payment` does not let an unpaid cart
101
+ > complete. Requirements and steps are separate on purpose.
102
+
103
+ ## Prefer a requirement to a workflow hook
104
+
105
+ A [workflow hook](workflows.md) on `carts.complete.validate`
106
+ can also reject a cart, and there are cases for it — anything needing a network call, or a
107
+ decision that only makes sense at the moment of completion.
108
+
109
+ For anything a customer has to fix, a requirement is better: it is reported on every
110
+ cart read, so the storefront can show it while they can still act, rather than failing
111
+ at the last step with an error they have to interpret.
112
+
113
+ ## Related
114
+
115
+ - [Carts](../core-concepts/carts.md) — reading requirements and completing a cart
116
+ - [Custom fields](../core-concepts/metafields.md) — storing your own data on a cart
117
+ - [Services & Workflows](workflows.md) — hooks that run during completion