@spree/docs 0.1.177 → 0.1.179

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 (117) hide show
  1. package/dist/developer/cli/quickstart.md +1 -1
  2. package/dist/developer/contributing/creating-an-extension.md +2 -2
  3. package/dist/developer/core-concepts/addresses.md +3 -3
  4. package/dist/developer/core-concepts/architecture.md +4 -4
  5. package/dist/developer/core-concepts/calculators.md +2 -2
  6. package/dist/developer/core-concepts/carts.md +207 -0
  7. package/dist/developer/core-concepts/channels.md +3 -3
  8. package/dist/developer/core-concepts/customers.md +1 -1
  9. package/dist/developer/core-concepts/events.md +1 -1
  10. package/dist/developer/core-concepts/fulfillments.md +278 -0
  11. package/dist/developer/core-concepts/imports-exports.md +6 -18
  12. package/dist/developer/core-concepts/inventory.md +3 -3
  13. package/dist/developer/core-concepts/media.md +1 -1
  14. package/dist/developer/core-concepts/metafields.md +1 -1
  15. package/dist/developer/core-concepts/orders.md +92 -339
  16. package/dist/developer/core-concepts/payments.md +1 -1
  17. package/dist/developer/core-concepts/promotions.md +68 -195
  18. package/dist/developer/core-concepts/returns-exchanges-claims.md +173 -0
  19. package/dist/developer/core-concepts/store-credits-gift-cards.md +16 -18
  20. package/dist/developer/core-concepts/stores.md +1 -1
  21. package/dist/developer/core-concepts/taxes-discounts-fees.md +199 -0
  22. package/dist/developer/core-concepts/taxes.md +3 -3
  23. package/dist/developer/create-spree-app/quickstart.md +1 -1
  24. package/dist/developer/customization/api.md +36 -11
  25. package/dist/developer/customization/configuration.md +208 -37
  26. package/dist/developer/customization/decorators.md +8 -8
  27. package/dist/developer/customization/permissions.md +34 -229
  28. package/dist/developer/customization/quickstart.md +159 -108
  29. package/dist/developer/customization/validations.md +1 -1
  30. package/dist/developer/dashboard/overview.md +1 -1
  31. package/dist/developer/how-to/build-a-b2b-store.md +21 -0
  32. package/dist/developer/how-to/build-a-marketplace.md +25 -0
  33. package/dist/developer/how-to/custom-api-authentication.md +1 -1
  34. package/dist/developer/how-to/custom-delivery-rate-provider.md +242 -0
  35. package/dist/developer/how-to/custom-document-numbers.md +154 -0
  36. package/dist/developer/how-to/custom-order-routing.md +1 -1
  37. package/dist/developer/how-to/custom-payment-method.md +1 -1
  38. package/dist/developer/how-to/custom-promotion.md +76 -115
  39. package/dist/developer/how-to/custom-report.md +2 -2
  40. package/dist/developer/how-to/custom-stock-splitter.md +5 -5
  41. package/dist/developer/how-to/sell-digital-products.md +20 -0
  42. package/dist/developer/multi-tenant/quickstart.md +1 -1
  43. package/dist/developer/providers/dam.md +14 -0
  44. package/dist/developer/providers/erp.md +31 -0
  45. package/dist/developer/providers/fulfillment.md +25 -0
  46. package/dist/developer/{deployment/telemetry.md → providers/observability.md} +1 -1
  47. package/dist/developer/providers/overview.md +44 -0
  48. package/dist/developer/providers/pim.md +25 -0
  49. package/dist/developer/providers/sso.md +20 -0
  50. package/dist/developer/sdk/admin/extending.md +1 -1
  51. package/dist/developer/sdk/admin/quickstart.md +1 -1
  52. package/dist/developer/sdk/admin/resources.md +1 -1
  53. package/dist/developer/sdk/extending.md +1 -1
  54. package/dist/developer/tutorial/admin-api.md +13 -0
  55. package/dist/developer/tutorial/dashboard-plugin.md +14 -0
  56. package/dist/developer/tutorial/events.md +7 -161
  57. package/dist/developer/tutorial/introduction.md +15 -35
  58. package/dist/developer/tutorial/model.md +7 -98
  59. package/dist/developer/tutorial/store-api.md +13 -0
  60. package/dist/developer/tutorial/storefront.md +12 -0
  61. package/dist/developer/tutorial/testing.md +7 -711
  62. package/dist/developer/upgrades/5.6-to-6.0.md +2 -2
  63. package/package.json +1 -1
  64. package/dist/developer/admin/admin.md +0 -214
  65. package/dist/developer/admin/authentication.md +0 -59
  66. package/dist/developer/admin/components.md +0 -711
  67. package/dist/developer/admin/custom-css.md +0 -256
  68. package/dist/developer/admin/custom-javascript.md +0 -116
  69. package/dist/developer/admin/extending-ui.md +0 -1839
  70. package/dist/developer/admin/form-builder.md +0 -444
  71. package/dist/developer/admin/helper-methods.md +0 -531
  72. package/dist/developer/admin/navigation.md +0 -805
  73. package/dist/developer/admin/tables.md +0 -490
  74. package/dist/developer/advanced/adding_spree_to_rails_app.md +0 -92
  75. package/dist/developer/core-concepts/adjustments.md +0 -113
  76. package/dist/developer/core-concepts/reports.md +0 -208
  77. package/dist/developer/core-concepts/shipments.md +0 -307
  78. package/dist/developer/core-concepts/users.md +0 -303
  79. package/dist/developer/customization/authentication.md +0 -100
  80. package/dist/developer/customization/checkout.md +0 -202
  81. package/dist/developer/customization/emails.md +0 -18
  82. package/dist/developer/customization/routes.md +0 -24
  83. package/dist/developer/multi-vendor/installation.md +0 -61
  84. package/dist/developer/multi-vendor/quickstart.md +0 -17
  85. package/dist/developer/tutorial/admin.md +0 -206
  86. package/dist/developer/tutorial/api.md +0 -606
  87. package/dist/developer/tutorial/extending-models.md +0 -393
  88. package/dist/developer/tutorial/sdk.md +0 -170
  89. package/dist/developer/upgrades/2.0-to-2.1.md +0 -46
  90. package/dist/developer/upgrades/2.1-to-2.2.md +0 -59
  91. package/dist/developer/upgrades/2.2-to-2.3.md +0 -44
  92. package/dist/developer/upgrades/2.3-to-2.4.md +0 -42
  93. package/dist/developer/upgrades/3.0-to-3.1.md +0 -47
  94. package/dist/developer/upgrades/3.1-to-3.2.md +0 -34
  95. package/dist/developer/upgrades/3.2-to-3.3.md +0 -70
  96. package/dist/developer/upgrades/3.3-to-3.4.md +0 -36
  97. package/dist/developer/upgrades/3.4-to-3.5.md +0 -44
  98. package/dist/developer/upgrades/3.5-to-3.6.md +0 -40
  99. package/dist/developer/upgrades/3.6-to-3.7.md +0 -62
  100. package/dist/developer/upgrades/3.7-to-4.0.md +0 -152
  101. package/dist/developer/upgrades/4.0-to-4.1.md +0 -92
  102. package/dist/developer/upgrades/4.1-to-4.2.md +0 -109
  103. package/dist/developer/upgrades/4.10-to-5.0.md +0 -131
  104. package/dist/developer/upgrades/4.2-to-4.3.md +0 -100
  105. package/dist/developer/upgrades/4.3-to-4.4.md +0 -125
  106. package/dist/developer/upgrades/4.4-to-4.5.md +0 -94
  107. package/dist/developer/upgrades/4.5-to-4.6.md +0 -119
  108. package/dist/developer/upgrades/4.6-to-4.7.md +0 -39
  109. package/dist/developer/upgrades/4.8-to-4.9.md +0 -24
  110. package/dist/developer/upgrades/4.9-to-4.10.md +0 -24
  111. package/dist/developer/upgrades/4.x-to-4.8.md +0 -52
  112. package/dist/developer/upgrades/5.0-to-5.1.md +0 -28
  113. package/dist/developer/upgrades/5.1-to-5.2.md +0 -131
  114. package/dist/developer/upgrades/5.2-to-5.3.md +0 -338
  115. package/dist/developer/upgrades/5.3-to-5.4.md +0 -277
  116. package/dist/developer/upgrades/5.4-to-5.5.md +0 -301
  117. package/dist/developer/upgrades/5.5-to-5.6.md +0 -207
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Spree CLI
3
- sidebarTitle: Overview
3
+ sidebarTitle: Spree CLI
4
4
  description: Install and use the @spree/cli to scaffold, run, build, and migrate Spree projects — and call the Admin API directly from the command line.
5
5
  ---
6
6
 
@@ -237,8 +237,8 @@ While this tutorial uses decorators to extend Spree's core behavior, modern Spre
237
237
  | React to model changes | [Events subscribers](../core-concepts/events.md) |
238
238
  | External service integration | [Webhooks](../core-concepts/webhooks.md) |
239
239
  | Replace core services | [Dependencies injection](../customization/dependencies.md) |
240
- | Add admin UI elements | [Admin Partials](../admin/extending-ui.md) |
241
- | Add admin menu items | [Admin Navigation](../admin/navigation.md) |
240
+ | Add admin UI elements | [Admin Partials](../dashboard/customization/quickstart.md) |
241
+ | Add admin menu items | [Admin Navigation](../dashboard/customization/navigation.md) |
242
242
 
243
243
  For example, if your extension needs to sync data with an external service when products are updated, use an Events subscriber instead of a decorator callback:
244
244
 
@@ -243,7 +243,7 @@ For countries without predefined states, addresses accept a free-text `state_nam
243
243
 
244
244
  ## Zones
245
245
 
246
- Zones group countries or states together for [tax](taxes.md) and [shipping](shipments.md) rules. A zone is either **country-based** or **state-based**.
246
+ Zones group countries or states together for [tax](taxes.md) and [shipping](fulfillments.md) rules. A zone is either **country-based** or **state-based**.
247
247
 
248
248
  **Examples:**
249
249
  - **EU VAT** (country zone) — Germany, France, Italy, Spain, ... → applies EU VAT rates
@@ -252,7 +252,7 @@ Zones group countries or states together for [tax](taxes.md) and [shipping](ship
252
252
 
253
253
  When a customer enters their address at checkout, Spree matches it against zones to determine:
254
254
  1. Which **tax rates** apply (see [Taxes](taxes.md))
255
- 2. Which **shipping methods** are available (see [Shipments](shipments.md))
255
+ 2. Which **shipping methods** are available (see [Shipments](fulfillments.md))
256
256
 
257
257
  Zones are configured in the admin dashboard — storefront developers don't interact with them directly via the API.
258
258
 
@@ -275,7 +275,7 @@ Here's how geography flows through a typical checkout:
275
275
 
276
276
  - [Markets](markets.md) — Multi-region commerce with currency, locale, and country grouping
277
277
  - [Taxes](taxes.md) — How zones and addresses affect taxation
278
- - [Shipments](shipments.md) — How zones and addresses affect shipping availability
278
+ - [Shipments](fulfillments.md) — How zones and addresses affect shipping availability
279
279
  - [Orders](orders.md) — Order billing and shipping addresses
280
280
  - [Account SDK guide](../sdk/store/account.md) — Managing the logged-in customer's address book with `client.customer.addresses`
281
281
  - [Store API authentication](../../api-reference/store-api/authentication.md) — How to obtain and send the customer JWT (`Authorization: Bearer` header)
@@ -57,11 +57,11 @@ flowchart TB
57
57
 
58
58
  2. **Shopping** — Customers add Variants to their cart, creating an [Order](orders.md) with Line Items
59
59
 
60
- 3. **Checkout** — The Order collects [Addresses](addresses.md), calculates [Shipping](shipments.md) options, and processes [Payments](payments.md)
60
+ 3. **Checkout** — The Order collects [Addresses](addresses.md), calculates [Shipping](fulfillments.md) options, and processes [Payments](payments.md)
61
61
 
62
- 4. **Fulfillment** — [Shipments](shipments.md) are created from Stock Locations, tracking individual [Inventory Units](inventory.md#inventory-units)
62
+ 4. **Fulfillment** — [Shipments](fulfillments.md) are created from Stock Locations, tracking individual [Inventory Units](inventory.md#inventory-units)
63
63
 
64
- 5. **Pricing & Adjustments** — [Taxes](taxes.md) and [Promotions](promotions.md) create [Adjustments](adjustments.md) that modify order totals
64
+ 5. **Pricing & Adjustments** — [Taxes](taxes.md) and [Promotions](promotions.md) create [Adjustments](taxes-discounts-fees.md) that modify order totals
65
65
 
66
66
  ## Core Model Relationships
67
67
 
@@ -172,5 +172,5 @@ Spree is distributed as a set of packages:
172
172
  - [Products](products.md) — Product catalog and variants
173
173
  - [Orders](orders.md) — Order lifecycle and state machine
174
174
  - [Payments](payments.md) — Payment processing
175
- - [Shipments](shipments.md) — Shipping and fulfillment
175
+ - [Shipments](fulfillments.md) — Shipping and fulfillment
176
176
  - [Customization Quickstart](../customization/quickstart.md) — How to extend Spree
@@ -45,7 +45,7 @@ erDiagram
45
45
  **Key relationships:**
46
46
  - **Calculator** computes amounts for various features
47
47
  - Used by **[Tax Rates](taxes.md)** to calculate tax amounts
48
- - Used by **[Shipping Methods](shipments.md)** to calculate shipping costs
48
+ - Used by **[Shipping Methods](fulfillments.md)** to calculate shipping costs
49
49
  - Used by **[Promotion Actions](promotions.md)** to calculate discounts
50
50
  - Calculators store [preferences (rates, percentages, etc.)](../customization/model-preferences.md) for their calculations
51
51
 
@@ -299,5 +299,5 @@ The module does not define `create_adjustment`, `update_adjustment`, or `compute
299
299
 
300
300
  ## Related Documentation
301
301
 
302
- - [Adjustments](adjustments.md) — the records calculators compute amounts for.
302
+ - [Adjustments](taxes-discounts-fees.md) — the records calculators compute amounts for.
303
303
  - [Dependencies](../customization/dependencies.md) — swap out calculators via the Dependencies system.
@@ -0,0 +1,207 @@
1
+ ---
2
+ title: Carts
3
+ description: How Spree models the shopping and checkout phase — a cart collects items and customer details, then completing it creates an order.
4
+ ---
5
+
6
+ ## Overview
7
+
8
+ A cart is everything that happens before a purchase is final: adding items, entering an address, choosing delivery, and paying. Completing a cart creates an [Order](orders.md) — a separate, permanent record.
9
+
10
+ The two are kept apart because they want opposite things. A cart changes constantly, tolerates half-finished states, and is often abandoned. An order is a financial record: it must not change quietly, and it has to keep saying what the customer actually agreed to pay.
11
+
12
+ ```mermaid
13
+ erDiagram
14
+ Cart ||--o{ LineItem : "has many"
15
+ Cart ||--o{ Fulfillment : "has many"
16
+ Cart ||--o| Order : "completes into"
17
+ Cart }o--|| Customer : "belongs to"
18
+
19
+ Cart {
20
+ string token
21
+ string email
22
+ string currency
23
+ string total
24
+ datetime completed_at
25
+ }
26
+
27
+ Order {
28
+ string number
29
+ string payment_status
30
+ string fulfillment_status
31
+ string total
32
+ }
33
+ ```
34
+
35
+ ## Cart attributes
36
+
37
+ | Attribute | Description |
38
+ |---|---|
39
+ | `id` | Cart ID, e.g. `cart_86Rf07xd4z` |
40
+ | `token` | Guest cart token — save it so a guest can return to their cart |
41
+ | `email` | Customer's email address |
42
+ | `currency` | Cart currency, e.g. `USD` |
43
+ | `total_quantity` | Total number of items |
44
+ | `requirements` | What the cart still needs before it can be completed |
45
+ | `item_total` / `display_item_total` | Sum of line item prices |
46
+ | `delivery_total` / `display_delivery_total` | Delivery cost |
47
+ | `tax_total` / `display_tax_total` | Total tax |
48
+ | `discount_total` / `display_discount_total` | Total discount |
49
+ | `total` / `display_total` | Cart total |
50
+ | `amount_due` / `display_amount_due` | Still to pay after store credit and gift cards |
51
+ | `warnings` | Items removed or changed since the customer last looked |
52
+ | `completed_at` | Set once checkout succeeded |
53
+
54
+ Every amount comes in two forms: `total` is the raw value (`"135.60"`) and [`display_total` is formatted for the currency](../../api-reference/store-api/monetary-amounts.md) (`"$135.60"`). Render the `display_` one.
55
+
56
+ > **NOTE:** Money is sent as a **string**, not a number. JavaScript numbers can't represent every decimal exactly — `0.1 + 0.2` gives `0.30000000000000004` — which is not something you want inside a price. Let Spree do the arithmetic and render the formatted strings.
57
+
58
+ ## Creating a cart and adding items
59
+
60
+
61
+ ```typescript Store SDK
62
+ // Create a cart
63
+ const cart = await client.carts.create()
64
+ // cart.token => "abc123" (save this for guest checkout)
65
+
66
+ // Add an item
67
+ await client.carts.items.create(cart.id, {
68
+ variant_id: 'var_xxx',
69
+ quantity: 2,
70
+ })
71
+
72
+ // Update quantity
73
+ await client.carts.items.update(cart.id, 'li_xxx', { quantity: 3 })
74
+
75
+ // Remove an item
76
+ await client.carts.items.delete(cart.id, 'li_xxx')
77
+ ```
78
+
79
+ ```bash cURL
80
+ # Create a cart
81
+ curl -X POST 'https://api.mystore.com/api/v3/store/carts' \
82
+ -H 'X-Spree-API-Key: pk_xxx'
83
+
84
+ # Add an item
85
+ curl -X POST 'https://api.mystore.com/api/v3/store/carts/cart_xxx/items' \
86
+ -H 'X-Spree-API-Key: pk_xxx' \
87
+ -H 'X-Spree-Token: abc123' \
88
+ -H 'Content-Type: application/json' \
89
+ -d '{ "variant_id": "var_xxx", "quantity": 2 }'
90
+
91
+ # Update quantity
92
+ curl -X PATCH 'https://api.mystore.com/api/v3/store/carts/cart_xxx/items/li_xxx' \
93
+ -H 'X-Spree-API-Key: pk_xxx' \
94
+ -H 'X-Spree-Token: abc123' \
95
+ -H 'Content-Type: application/json' \
96
+ -d '{ "quantity": 3 }'
97
+ ```
98
+
99
+
100
+ Every change returns the whole cart with new totals, so you never need a second request to refresh the summary.
101
+
102
+ When a guest signs in, attach their cart to the account:
103
+
104
+ ```typescript Store SDK
105
+ await client.carts.associate(cartId, { spreeToken: 'abc123' })
106
+ ```
107
+
108
+ ## Checkout requirements
109
+
110
+ A cart tells you what it still needs before it can be completed:
111
+
112
+ ```json
113
+ {
114
+ "requirements": [
115
+ {
116
+ "step": "address",
117
+ "field": "email",
118
+ "code": "email_required",
119
+ "message": "Email address is required"
120
+ }
121
+ ]
122
+ }
123
+ ```
124
+
125
+ An empty `requirements` array means the cart is ready.
126
+
127
+ 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.
128
+
129
+ ## Checkout
130
+
131
+ **Step 1: Add the customer's details**
132
+
133
+ ```typescript Store SDK
134
+ await client.carts.update(cartId, {
135
+ email: 'john@example.com',
136
+ shipping_address: {
137
+ first_name: 'John', last_name: 'Doe',
138
+ address1: '123 Main St', city: 'Los Angeles',
139
+ country_code: 'US', state_abbr: 'CA', postal_code: '90001',
140
+ },
141
+ })
142
+ ```
143
+
144
+ **Step 2: Choose delivery**
145
+
146
+ Each [fulfillment](fulfillments.md) offers delivery rates. Pick one per fulfillment.
147
+
148
+ ```typescript Store SDK
149
+ const cart = await client.carts.get(cartId)
150
+ await client.carts.fulfillments.update(cartId, cart.fulfillments[0].id, {
151
+ selected_delivery_rate_id: 'rate_xxx',
152
+ })
153
+ ```
154
+
155
+ **Step 3: Take payment**
156
+
157
+ For providers like Stripe, create a payment session and confirm it with their own SDK.
158
+
159
+ ```typescript Store SDK
160
+ const session = await client.carts.paymentSessions.create(cartId, {
161
+ payment_method_id: 'pm_xxx',
162
+ })
163
+ // session.external_data.client_secret => use with Stripe.js
164
+
165
+ await client.carts.paymentSessions.complete(cartId, session.id)
166
+ ```
167
+
168
+ **Step 4: Complete the cart**
169
+
170
+ ```typescript Store SDK
171
+ const order = await client.carts.complete(cartId)
172
+ ```
173
+
174
+ You get back an [Order](orders.md).
175
+
176
+
177
+ Discount codes, gift cards and store credit can be applied any time before completion:
178
+
179
+ ```typescript Store SDK
180
+ await client.carts.discountCodes.apply(cartId, 'SUMMER20')
181
+ await client.carts.giftCards.apply(cartId, 'GIFT-XXXX')
182
+ await client.carts.storeCredits.apply(cartId)
183
+ ```
184
+
185
+ ## What completion guarantees
186
+
187
+ Completing a cart moves real money, so Spree is careful about it:
188
+
189
+ **Double submission is safe.** A customer double-clicking "Place order" gets the same order back rather than a second charge. This holds even when your app runs on several servers.
190
+
191
+ **An interrupted completion recovers.** If something dies after the customer was charged, trying again finishes the job instead of charging twice.
192
+
193
+ **Totals are worked out at the last moment.** The amount charged is calculated when the cart is completed, not taken from an earlier request — so a price or promotion that changed while the customer was reviewing can't lead to the wrong charge.
194
+
195
+ If a cart can't be completed you get told why — a payment failure, or unmet [requirements](#checkout-requirements) — and the cart is left alone so the customer can fix it and try again.
196
+
197
+ ## Abandoned carts
198
+
199
+ Carts emit `cart.created`, `cart.updated` and `cart.deleted` [events](events.md), which also reach [webhooks](webhooks.md) — enough to drive abandonment email without polling for changes.
200
+
201
+ Completed carts are kept rather than deleted, and an order records which cart it came from, so you can measure conversion. Abandoned carts are cleared out on a schedule you control; carts with a payment in progress are never removed.
202
+
203
+ ## Related
204
+
205
+ - [Orders](orders.md) — the record a cart becomes
206
+ - [Payments](payments.md) — payment methods and sessions
207
+ - [Fulfillments](fulfillments.md) — delivery options
@@ -22,7 +22,7 @@ Every store ships with one default channel named *Online Store*. You can add mor
22
22
  | `default` | Exactly one channel per store is the default. Used as a fallback when no channel header is present and as the auto-publish target for new products | `true` |
23
23
  | `storefront_access` | Controls what an anonymous visitor may see: `public`, `prices_hidden`, or `login_required`. Unset inherits the store's setting. See [Storefront Access Gating](#storefront-access-gating) | `login_required` |
24
24
  | `guest_checkout` | Whether an order may be placed without an account on this channel. Unset inherits the store's setting | `false` |
25
- | `preferred_order_routing_strategy` | Optional per-channel override of the store's [Order Routing](shipments.md#order-routing) strategy | `Spree::OrderRouting::Strategy::Rules` |
25
+ | `preferred_order_routing_strategy` | Optional per-channel override of the store's [Order Routing](fulfillments.md#order-routing) strategy | `Spree::OrderRouting::Strategy::Rules` |
26
26
 
27
27
  `code` is normalized to a URL-safe slug on save — `POS` becomes `pos`, `Point of Sale!` becomes `point-of-sale`. Leaving `code` blank derives it from `name`.
28
28
 
@@ -89,7 +89,7 @@ Product status (`draft` / `active` / `archived`) is the **outer gate**: a Draft
89
89
 
90
90
  Every order is attributed to one channel. The channel is set from the `X-Spree-Channel` header on cart creation, from the merchant's selection on the "New order" form, or defaults to the store's primary channel.
91
91
 
92
- This attribution drives reporting (best-selling by channel, revenue per channel) and per-channel order routing — see [Order Routing](shipments.md#order-routing).
92
+ This attribution drives reporting (best-selling by channel, revenue per channel) and per-channel order routing — see [Order Routing](fulfillments.md#order-routing).
93
93
 
94
94
  ### Storefront Access Gating
95
95
 
@@ -217,7 +217,7 @@ The write contract is **full-set**: the array represents the complete desired st
217
217
  - [Stores](stores.md) — Channels belong to a store
218
218
  - [Markets](markets.md) — Different from channels: markets segment geography/currency, channels segment selling surfaces
219
219
  - [Products](products.md) — Product catalog and publication
220
- - [Order Routing](shipments.md#order-routing) — Channels can override the store's routing strategy
220
+ - [Order Routing](fulfillments.md#order-routing) — Channels can override the store's routing strategy
221
221
  - [Store SDK: Products](../sdk/store/products.md) — Channel-scoped product listing and filtering
222
222
  - [Admin SDK: Resources](../sdk/admin/resources.md) — How `adminClient.channels.addProducts` and other resource methods are structured
223
223
  - [Wholesale Portal](../storefront/nextjs/wholesale.md) — A gated channel driving the Next.js storefront's B2B surface
@@ -305,5 +305,5 @@ Customers don't need to register to purchase. Guest checkout uses an order token
305
305
  - [Account (Store SDK)](../sdk/store/account.md) — Task-oriented walkthrough of registration, login, profile, addresses, and order history
306
306
  - [Addresses](addresses.md) — Customer address management
307
307
  - [Orders](orders.md) — Order history and checkout
308
- - [Authentication](../customization/authentication.md) — Custom authentication setup
308
+ - [Authentication](../providers/sso.md) — Custom authentication setup
309
309
  - [Staff & Roles](staff-roles.md) — Admin users and permissions
@@ -641,4 +641,4 @@ The base class also provides helper methods:
641
641
  - [Webhooks & Events Reference](../../api-reference/webhooks-events.md) - Catalog of event and webhook payloads
642
642
  - [Customization Quickstart](../customization/quickstart.md) - Overview of all customization options
643
643
  - [Decorators](../customization/decorators.md) - When to use decorators vs events
644
- - [Checkout Flow](../customization/checkout.md) - Using events in checkout customization
644
+ - [Checkout Flow](carts.md) - Using events in checkout customization
@@ -0,0 +1,278 @@
1
+ ---
2
+ title: Fulfillments
3
+ description: How Spree models getting items to the customer — fulfillments, delivery methods and zones, and delivery types covering shipping, digital and pickup.
4
+ ---
5
+
6
+ ## Overview
7
+
8
+ A fulfillment is one batch of items going to the customer by one method — a parcel from a warehouse, a digital download, or an order waiting at a pickup counter. An order gets one fulfillment per combination of stock location and delivery method, so an order sourced from two warehouses has two.
9
+
10
+ The word is deliberately broader than "shipment". A digital download has no carrier, no address and no package, and click-and-collect ships nowhere at all — a fulfillment covers all of them without dragging along fields that make no sense for most.
11
+
12
+ ```mermaid
13
+ erDiagram
14
+ Order ||--o{ Fulfillment : "has many"
15
+ Fulfillment ||--o{ FulfillmentItem : "has many"
16
+ Fulfillment ||--o{ DeliveryRate : "offers"
17
+ Fulfillment }o--|| StockLocation : "sourced from"
18
+ DeliveryRate }o--|| DeliveryMethod : "priced by"
19
+ DeliveryMethod }o--o{ DeliveryZone : "serves"
20
+
21
+ Fulfillment {
22
+ string number
23
+ string status
24
+ string tracking
25
+ string cost
26
+ datetime fulfilled_at
27
+ }
28
+ ```
29
+
30
+ ## Fulfillment attributes
31
+
32
+ | Attribute | Description |
33
+ |---|---|
34
+ | `id` | Fulfillment ID, e.g. `ful_86Rf07xd4z` |
35
+ | `number` | Fulfillment number, e.g. `F123456789` |
36
+ | `status` | See [statuses](#statuses) below |
37
+ | `tracking` | Carrier tracking number, once it's on its way |
38
+ | `cost` / `display_cost` | Delivery cost for this fulfillment |
39
+ | `delivery_rates` | The options the customer can choose from |
40
+ | `selected_delivery_rate_id` | The chosen option |
41
+ | `fulfilled_at` | When it went out |
42
+
43
+ ## Statuses
44
+
45
+ A fulfillment's status says what **you** did with the parcel — nothing else.
46
+
47
+ | Value | Meaning |
48
+ |---|---|
49
+ | `unfulfilled` | Still with you. Nothing has left |
50
+ | `fulfilled` | It went out — handed to the carrier, or waiting at the counter for a pickup order |
51
+ | `delivered` | The customer has it |
52
+ | `canceled` | It won't be sent |
53
+
54
+ Statuses only move forward: once something has gone out it stays gone out, and a later payment problem changes the order's [payment status](orders.md#statuses) rather than the fulfillment.
55
+
56
+ Whether a fulfillment *can* go out — the order is paid, nothing is on backorder — is checked when you fulfill it, and the answer comes back with a reason. It is not baked into the status, so a refund never moves a parcel backwards. Staff who need to ship against an invoice can pass `force`.
57
+
58
+ ### Where the parcel actually is
59
+
60
+ Carrier reports live on a second field, `tracking_status`, and never change `status`:
61
+
62
+ | Value | Meaning |
63
+ |---|---|
64
+ | `pre_transit` | Label made, carrier hasn't got it yet |
65
+ | `in_transit` | On its way |
66
+ | `out_for_delivery` | On the van |
67
+ | `available_for_pickup` | Waiting at a carrier location |
68
+ | `delivered` | Carrier says it arrived |
69
+ | `return_to_sender` | Coming back to you |
70
+ | `failure` | Delivery failed — a bad address, damage, a refused parcel |
71
+ | `unknown` | The carrier said something we don't recognise |
72
+
73
+ Keeping the two apart is the point. A parcel that bounces still shows `fulfilled`, because you did hand it over; the trouble shows up as `return_to_sender` and you decide what to do about it. The one report that crosses over is delivery, which also sets `delivered` and `delivered_at`.
74
+
75
+ `delivered_at` is what return windows and the EU withdrawal period count from, so it records when the carrier says the parcel arrived rather than when you heard about it.
76
+
77
+ Carriers set this through their provider's webhook. With no carrier integration, staff mark receipt by hand — `markDelivered` in the Admin SDK — which is also how pickup orders get closed out.
78
+
79
+ ## Delivery types
80
+
81
+ Every delivery method has a type that decides how it behaves:
82
+
83
+ | Type | Behaviour |
84
+ |---|---|
85
+ | `shipping` | Physical delivery to an address. Someone marks it sent. |
86
+ | `digital` | Available the moment the order is placed. No address needed. |
87
+ | `pickup` | Collection from one of your own stock locations. |
88
+ | `pickup_point` | Collection from a third-party point — a locker or partner shop. |
89
+
90
+ This is what lets a store sell a downloadable album and a vinyl record in the same order without any special handling in your storefront: the download is ready immediately, the record gets a tracking number.
91
+
92
+ ## Choosing delivery at checkout
93
+
94
+ Each fulfillment on a cart offers delivery rates. The customer picks one per fulfillment.
95
+
96
+
97
+ ```typescript Store SDK
98
+ const cart = await client.carts.get(cartId)
99
+
100
+ cart.fulfillments[0].delivery_rates
101
+ // => [{ id: 'rate_xxx', name: 'DHL Express', display_cost: '$12.00',
102
+ // estimated_delivery_date: '2026-08-07' }, ...]
103
+
104
+ await client.carts.fulfillments.update(cartId, cart.fulfillments[0].id, {
105
+ selected_delivery_rate_id: 'rate_xxx',
106
+ })
107
+ ```
108
+
109
+ ```bash cURL
110
+ curl -X PATCH 'https://api.mystore.com/api/v3/store/carts/cart_xxx/fulfillments/ful_xxx' \
111
+ -H 'X-Spree-API-Key: pk_xxx' \
112
+ -H 'X-Spree-Token: abc123' \
113
+ -H 'Content-Type: application/json' \
114
+ -d '{ "selected_delivery_rate_id": "rate_xxx" }'
115
+ ```
116
+
117
+
118
+ Rates carry more than a price — carrier, service level and estimated delivery date — so you can show "DHL Express, arrives Tuesday" instead of just a number.
119
+
120
+ ## Pickup
121
+
122
+ For pickup methods, ask the API where the customer can collect:
123
+
124
+
125
+ ```typescript Store SDK
126
+ // Your own pickup-enabled locations
127
+ const locations = await client.deliveryMethods.pickupLocations('dm_xxx')
128
+
129
+ // Third-party pickup points near the customer
130
+ const points = await client.deliveryMethods.pickupPoints('dm_xxx', {
131
+ latitude: 34.0522,
132
+ longitude: -118.2437,
133
+ })
134
+ ```
135
+
136
+ ```bash cURL
137
+ curl 'https://api.mystore.com/api/v3/store/delivery_methods/dm_xxx/pickup_locations' \
138
+ -H 'X-Spree-API-Key: pk_xxx'
139
+ ```
140
+
141
+
142
+ ## Managing fulfillments
143
+
144
+
145
+ ```typescript Admin SDK
146
+ // What's still with you
147
+ const fulfillments = await adminClient.orders.fulfillments.list('or_xxx')
148
+
149
+ // Add tracking, then mark it sent
150
+ await adminClient.orders.fulfillments.update('or_xxx', 'ful_xxx', {
151
+ tracking: '1Z999AA10123456784',
152
+ })
153
+ await adminClient.orders.fulfillments.fulfill('or_xxx', 'ful_xxx')
154
+
155
+ // Ship only some of it — the chosen units split off and ship,
156
+ // the rest stays open
157
+ await adminClient.orders.fulfillments.fulfill('or_xxx', 'ful_xxx', {
158
+ items: [{ item_id: 'li_xxx', quantity: 1 }],
159
+ })
160
+
161
+ // Ship against an unpaid invoice — your call, not the API's
162
+ await adminClient.orders.fulfillments.fulfill('or_xxx', 'ful_xxx', { force: true })
163
+
164
+ // Record receipt yourself when no carrier reports it for you
165
+ await adminClient.orders.fulfillments.markDelivered('or_xxx', 'ful_xxx')
166
+
167
+ // Split it when only part can go now
168
+ await adminClient.orders.fulfillments.split('or_xxx', 'ful_xxx', {
169
+ items: [{ fulfillment_item_id: 'fi_xxx', quantity: 1 }],
170
+ })
171
+
172
+ // Cancel
173
+ await adminClient.orders.fulfillments.cancel('or_xxx', 'ful_xxx')
174
+ ```
175
+
176
+ ```bash cURL
177
+ curl -X PATCH 'https://api.mystore.com/api/v3/admin/orders/or_xxx/fulfillments/ful_xxx' \
178
+ -H 'X-Spree-API-Key: sk_xxx' \
179
+ -H 'Content-Type: application/json' \
180
+ -d '{ "tracking": "1Z999AA10123456784" }'
181
+
182
+ curl -X PATCH 'https://api.mystore.com/api/v3/admin/orders/or_xxx/fulfillments/ful_xxx/fulfill' \
183
+ -H 'X-Spree-API-Key: sk_xxx'
184
+ ```
185
+
186
+
187
+ ## Delivery methods and zones
188
+
189
+ A **delivery method** is what the customer picks — "Standard", "Express", "Collect in store". A **delivery zone** decides where it's available.
190
+
191
+ Zones match on country and state, and also on postal code prefixes and ranges, so you can offer same-day delivery to a handful of city postcodes without listing every one:
192
+
193
+ ```typescript Admin SDK
194
+ await adminClient.deliveryZones.create({
195
+ name: 'London same-day',
196
+ members: [
197
+ { member_type: 'postal_code', country_code: 'GB', postal_code_prefix: 'SW1' },
198
+ { member_type: 'postal_code', country_code: 'GB', postal_code_from: 'EC1A', postal_code_to: 'EC1V' },
199
+ ],
200
+ })
201
+ ```
202
+
203
+ A zone member is either a whole country, a state, or a postal code prefix or range.
204
+
205
+ > **NOTE:** Delivery zones cover delivery only. Tax is handled separately — see [Taxes](taxes.md).
206
+
207
+ ### Pricing and rules
208
+
209
+ Delivery is priced either by a rule you configure in the dashboard — flat rate, per item, price bands — or by asking a carrier for live rates.
210
+
211
+ Methods can also carry conditions that decide when they're offered at all: free shipping over a certain order value, or heavy items excluded from letter post. These are applied in one place, so both kinds of pricing respect them.
212
+
213
+ ## Adding your own statuses
214
+
215
+ A made-to-order business — print on demand, furniture built after purchase —
216
+ has a stage between "order placed" and "handed to the carrier". There are two
217
+ ways to model it, and the lighter one is usually right.
218
+
219
+ **Track production in your own model, gate handover with a hook.** A production
220
+ pipeline usually has more resolution than one word — queued, printing, quality
221
+ check, packed — and that detail belongs in your own tables or your
222
+ integration's metadata, not in the parcel's lifecycle vocabulary. What core
223
+ needs to know is only *whether the parcel may go out yet*, and that is exactly
224
+ what the fulfill workflow's validate hook expresses:
225
+
226
+ ```ruby
227
+ Spree.hooks.register('fulfillments.fulfill.validate') do |flow|
228
+ work_order = MyApp::WorkOrder.find_by(fulfillment: flow.fulfillment)
229
+ flow.reject!('still in production') unless work_order&.completed?
230
+ end
231
+ ```
232
+
233
+ The fulfillment stays `unfulfilled` while you build the thing; your storefront
234
+ renders "we're making your furniture" as presentation, the same way pickup
235
+ orders render `fulfilled` as "ready for pickup".
236
+
237
+ **Add a real status when merchants need to act on the stage.** If staff filter
238
+ by it, reports group by it, or webhooks fire on entering it, make it a
239
+ first-class status:
240
+
241
+ ```ruby
242
+ # an initializer
243
+ Spree::Fulfillment.add_status('in_production', after: 'unfulfilled')
244
+ ```
245
+
246
+ That gives you the `in_production?` predicate, the `.in_production` scope and
247
+ a valid value. Moving into it is deliberately not declarative — write a small
248
+ workflow, which is also where the printer submission or the workshop handoff
249
+ belongs:
250
+
251
+ ```ruby
252
+ class MyApp::Fulfillments::StartProduction < Spree::Workflow
253
+ def perform(fulfillment:)
254
+ super
255
+ step :ensure_startable # refuse unless fulfillment.unfulfilled?
256
+ step :mark_in_production # update!(status: 'in_production') + publish an event
257
+ external_step :submit_to_printer
258
+ success(fulfillment.reload)
259
+ end
260
+ end
261
+ ```
262
+
263
+ Core's own actions keep working on your status without overrides: the guards
264
+ ask whether the parcel has *already gone out*, not whether it holds one of the
265
+ built-in values, so an `in_production` fulfillment can still be fulfilled or
266
+ canceled. Statuses are additive only — core workflows guard on core statuses,
267
+ so removing one would silently break them — and confirmed receipt still only
268
+ follows handover.
269
+
270
+ ## Events
271
+
272
+ Fulfillments publish [events](events.md) — `fulfillment.created`, `fulfillment.fulfilled`, `fulfillment.delivered`, `fulfillment.canceled` — which also reach [webhooks](webhooks.md). Use them to notify a customer, push to a warehouse system, or send tracking emails.
273
+
274
+ ## Related
275
+
276
+ - [Orders](orders.md) — how delivery status rolls up
277
+ - [Inventory](inventory.md) — stock locations and availability
278
+ - [Returns, Exchanges & Claims](returns-exchanges-claims.md) — items coming back
@@ -270,24 +270,12 @@ module Spree
270
270
  has_many :mappings # Field mappings
271
271
  has_many :rows # CSV rows to process
272
272
 
273
- # State machine
274
- state_machine initial: :pending do
275
- event :start_mapping do
276
- transition to: :mapping
277
- end
278
- event :complete_mapping do
279
- transition from: :mapping, to: :completed_mapping
280
- end
281
- event :start_processing do
282
- transition from: :completed_mapping, to: :processing
283
- end
284
- event :complete do
285
- transition from: :processing, to: :completed
286
- end
287
- event :fail do
288
- transition to: :failed
289
- end
290
- end
273
+ # Statuses. Each move between them is a workflow — Spree::Imports::
274
+ # StartMapping, CompleteMapping, StartProcessing, Complete and
275
+ # RetryFailedRows — so every step of the pipeline can be hooked.
276
+ include Spree::HasStatus
277
+ has_status :pending, :mapping, :completed_mapping, :processing, :completed, :failed,
278
+ default: :pending
291
279
 
292
280
  # Key methods
293
281
  def import_schema # Returns schema class instance