@spree/docs 0.1.176 → 0.1.178

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 (118) hide show
  1. package/dist/api-reference/store.yaml +48 -48
  2. package/dist/developer/cli/quickstart.md +1 -1
  3. package/dist/developer/contributing/creating-an-extension.md +2 -2
  4. package/dist/developer/core-concepts/addresses.md +3 -3
  5. package/dist/developer/core-concepts/architecture.md +4 -4
  6. package/dist/developer/core-concepts/calculators.md +2 -2
  7. package/dist/developer/core-concepts/carts.md +207 -0
  8. package/dist/developer/core-concepts/channels.md +3 -3
  9. package/dist/developer/core-concepts/customers.md +1 -1
  10. package/dist/developer/core-concepts/events.md +1 -1
  11. package/dist/developer/core-concepts/fulfillments.md +278 -0
  12. package/dist/developer/core-concepts/imports-exports.md +6 -18
  13. package/dist/developer/core-concepts/inventory.md +3 -3
  14. package/dist/developer/core-concepts/media.md +1 -1
  15. package/dist/developer/core-concepts/metafields.md +1 -1
  16. package/dist/developer/core-concepts/orders.md +92 -339
  17. package/dist/developer/core-concepts/payments.md +1 -1
  18. package/dist/developer/core-concepts/promotions.md +68 -195
  19. package/dist/developer/core-concepts/returns-exchanges-claims.md +173 -0
  20. package/dist/developer/core-concepts/store-credits-gift-cards.md +16 -18
  21. package/dist/developer/core-concepts/stores.md +1 -1
  22. package/dist/developer/core-concepts/taxes-discounts-fees.md +199 -0
  23. package/dist/developer/core-concepts/taxes.md +3 -3
  24. package/dist/developer/create-spree-app/quickstart.md +1 -1
  25. package/dist/developer/customization/api.md +36 -11
  26. package/dist/developer/customization/configuration.md +207 -37
  27. package/dist/developer/customization/decorators.md +8 -8
  28. package/dist/developer/customization/permissions.md +34 -229
  29. package/dist/developer/customization/quickstart.md +159 -108
  30. package/dist/developer/customization/validations.md +1 -1
  31. package/dist/developer/dashboard/overview.md +1 -1
  32. package/dist/developer/how-to/build-a-b2b-store.md +21 -0
  33. package/dist/developer/how-to/build-a-marketplace.md +25 -0
  34. package/dist/developer/how-to/custom-api-authentication.md +1 -1
  35. package/dist/developer/how-to/custom-delivery-rate-provider.md +242 -0
  36. package/dist/developer/how-to/custom-document-numbers.md +154 -0
  37. package/dist/developer/how-to/custom-order-routing.md +1 -1
  38. package/dist/developer/how-to/custom-payment-method.md +1 -1
  39. package/dist/developer/how-to/custom-promotion.md +76 -115
  40. package/dist/developer/how-to/custom-report.md +2 -2
  41. package/dist/developer/how-to/custom-stock-splitter.md +5 -5
  42. package/dist/developer/how-to/sell-digital-products.md +20 -0
  43. package/dist/developer/multi-tenant/quickstart.md +1 -1
  44. package/dist/developer/providers/dam.md +14 -0
  45. package/dist/developer/providers/erp.md +31 -0
  46. package/dist/developer/providers/fulfillment.md +25 -0
  47. package/dist/developer/{deployment/telemetry.md → providers/observability.md} +1 -1
  48. package/dist/developer/providers/overview.md +44 -0
  49. package/dist/developer/providers/pim.md +25 -0
  50. package/dist/developer/providers/sso.md +20 -0
  51. package/dist/developer/sdk/admin/extending.md +1 -1
  52. package/dist/developer/sdk/admin/quickstart.md +1 -1
  53. package/dist/developer/sdk/admin/resources.md +1 -1
  54. package/dist/developer/sdk/extending.md +1 -1
  55. package/dist/developer/tutorial/admin-api.md +13 -0
  56. package/dist/developer/tutorial/dashboard-plugin.md +14 -0
  57. package/dist/developer/tutorial/events.md +7 -161
  58. package/dist/developer/tutorial/introduction.md +15 -35
  59. package/dist/developer/tutorial/model.md +7 -98
  60. package/dist/developer/tutorial/store-api.md +13 -0
  61. package/dist/developer/tutorial/storefront.md +12 -0
  62. package/dist/developer/tutorial/testing.md +7 -711
  63. package/dist/developer/upgrades/5.6-to-6.0.md +2 -2
  64. package/package.json +1 -1
  65. package/dist/developer/admin/admin.md +0 -214
  66. package/dist/developer/admin/authentication.md +0 -59
  67. package/dist/developer/admin/components.md +0 -711
  68. package/dist/developer/admin/custom-css.md +0 -256
  69. package/dist/developer/admin/custom-javascript.md +0 -116
  70. package/dist/developer/admin/extending-ui.md +0 -1839
  71. package/dist/developer/admin/form-builder.md +0 -444
  72. package/dist/developer/admin/helper-methods.md +0 -531
  73. package/dist/developer/admin/navigation.md +0 -805
  74. package/dist/developer/admin/tables.md +0 -490
  75. package/dist/developer/advanced/adding_spree_to_rails_app.md +0 -92
  76. package/dist/developer/core-concepts/adjustments.md +0 -113
  77. package/dist/developer/core-concepts/reports.md +0 -208
  78. package/dist/developer/core-concepts/shipments.md +0 -307
  79. package/dist/developer/core-concepts/users.md +0 -303
  80. package/dist/developer/customization/authentication.md +0 -100
  81. package/dist/developer/customization/checkout.md +0 -202
  82. package/dist/developer/customization/emails.md +0 -18
  83. package/dist/developer/customization/routes.md +0 -24
  84. package/dist/developer/multi-vendor/installation.md +0 -61
  85. package/dist/developer/multi-vendor/quickstart.md +0 -17
  86. package/dist/developer/tutorial/admin.md +0 -206
  87. package/dist/developer/tutorial/api.md +0 -606
  88. package/dist/developer/tutorial/extending-models.md +0 -393
  89. package/dist/developer/tutorial/sdk.md +0 -170
  90. package/dist/developer/upgrades/2.0-to-2.1.md +0 -46
  91. package/dist/developer/upgrades/2.1-to-2.2.md +0 -59
  92. package/dist/developer/upgrades/2.2-to-2.3.md +0 -44
  93. package/dist/developer/upgrades/2.3-to-2.4.md +0 -42
  94. package/dist/developer/upgrades/3.0-to-3.1.md +0 -47
  95. package/dist/developer/upgrades/3.1-to-3.2.md +0 -34
  96. package/dist/developer/upgrades/3.2-to-3.3.md +0 -70
  97. package/dist/developer/upgrades/3.3-to-3.4.md +0 -36
  98. package/dist/developer/upgrades/3.4-to-3.5.md +0 -44
  99. package/dist/developer/upgrades/3.5-to-3.6.md +0 -40
  100. package/dist/developer/upgrades/3.6-to-3.7.md +0 -62
  101. package/dist/developer/upgrades/3.7-to-4.0.md +0 -152
  102. package/dist/developer/upgrades/4.0-to-4.1.md +0 -92
  103. package/dist/developer/upgrades/4.1-to-4.2.md +0 -109
  104. package/dist/developer/upgrades/4.10-to-5.0.md +0 -131
  105. package/dist/developer/upgrades/4.2-to-4.3.md +0 -100
  106. package/dist/developer/upgrades/4.3-to-4.4.md +0 -125
  107. package/dist/developer/upgrades/4.4-to-4.5.md +0 -94
  108. package/dist/developer/upgrades/4.5-to-4.6.md +0 -119
  109. package/dist/developer/upgrades/4.6-to-4.7.md +0 -39
  110. package/dist/developer/upgrades/4.8-to-4.9.md +0 -24
  111. package/dist/developer/upgrades/4.9-to-4.10.md +0 -24
  112. package/dist/developer/upgrades/4.x-to-4.8.md +0 -52
  113. package/dist/developer/upgrades/5.0-to-5.1.md +0 -28
  114. package/dist/developer/upgrades/5.1-to-5.2.md +0 -131
  115. package/dist/developer/upgrades/5.2-to-5.3.md +0 -338
  116. package/dist/developer/upgrades/5.3-to-5.4.md +0 -277
  117. package/dist/developer/upgrades/5.4-to-5.5.md +0 -301
  118. package/dist/developer/upgrades/5.5-to-5.6.md +0 -207
@@ -1,18 +1,17 @@
1
1
  ---
2
2
  title: Build Custom Promotion Rules & Actions
3
- description: Step-by-step guide to creating custom promotion rules and actions for business-specific eligibility logic and discount behaviors.
3
+ description: Step-by-step guide to creating custom promotion rules, discount actions, and adjusters for business-specific eligibility logic and charges.
4
4
  ---
5
5
 
6
6
  ## Overview
7
7
 
8
- Spree's promotion system is built around two extension points: **Rules** (eligibility conditions) and **Actions** (what happens when a promotion applies). While Spree ships with a comprehensive set of [built-in rules and actions](../core-concepts/promotions.md#rules), you can create custom ones for business-specific logic.
8
+ Spree's promotion system has three extension points:
9
9
 
10
- This guide covers:
11
- - Creating a custom promotion rule with admin UI
12
- - Creating a custom promotion action with a calculator
13
- - Understanding the `eligible?`, `actionable?`, and `perform` contracts
10
+ - **Rules** — conditions that decide when a promotion applies
11
+ - **Actions** what an applied promotion does, usually writing [Discount](../core-concepts/taxes-discounts-fees.md#discounts) rows
12
+ - **Adjusters** for discounts and fees that aren't promotions at all (loyalty pricing, gift wrap fees, payment surcharges)
14
13
 
15
- Before starting, make sure you understand [how promotions work in Spree](../core-concepts/promotions.md).
14
+ Spree ships with a comprehensive set of [built-in rules and actions](../core-concepts/promotions.md#rules). This guide shows how to build your own of each kind.
16
15
 
17
16
  ## Custom Promotion Rules
18
17
 
@@ -30,7 +29,7 @@ module Spree
30
29
  preference :quantity, :integer, default: 5
31
30
 
32
31
  def applicable?(promotable)
33
- promotable.is_a?(Spree::Order)
32
+ promotable.is_a?(Spree::Order) || promotable.is_a?(Spree::Cart)
34
33
  end
35
34
 
36
35
  def eligible?(order, options = {})
@@ -56,12 +55,14 @@ end
56
55
 
57
56
  | Method | Required | Description |
58
57
  |--------|----------|-------------|
59
- | `applicable?(promotable)` | Yes | Returns `true` if this rule type can evaluate the promotable (usually `promotable.is_a?(Spree::Order)`) |
58
+ | `applicable?(promotable)` | Yes | Returns `true` if this rule type can evaluate the promotable — accept both carts and orders (see the note below) |
60
59
  | `eligible?(promotable, options = {})` | Yes | Returns `true` if the promotable meets this rule's conditions. Add messages to `eligibility_errors` to explain why not. |
61
- | `actionable?(line_item)` | No | Returns `true` if a specific line item should receive the promotion's action. Defaults to `true`. Override this for rules that target specific items (like product or category rules). |
60
+ | `actionable?(line_item)` | No | Returns `true` if a specific line item should receive the promotion's discount. Defaults to `true`. Override this for rules that target specific items (like product or category rules). |
62
61
 
63
62
  The `options` hash passed to `eligible?` can include `:user`, `:email`, and other context from the checkout flow.
64
63
 
64
+ > **WARNING:** Accept **both** `Spree::Cart` and `Spree::Order` in `applicable?`. Promotions are evaluated on every cart change, long before the cart becomes an order, and in Spree 6 a cart is its own model rather than an unfinished order. A rule guarding on `Spree::Order` alone silently never applies during checkout — the promotion just appears not to work. Every built-in rule accepts both.
65
+
65
66
  #### Using Preferences
66
67
 
67
68
  Rules use Spree's preference system for configuration. Each preference creates getter/setter methods automatically:
@@ -81,43 +82,30 @@ Available types: `:string`, `:integer`, `:decimal`, `:boolean`, `:array`.
81
82
 
82
83
  ### Step 2: Register the Rule
83
84
 
84
- Add your rule to the promotion configuration so it appears in the admin panel:
85
-
86
85
  ```ruby config/initializers/spree.rb
87
86
  Rails.application.config.after_initialize do
88
87
  Spree.promotions.rules << Spree::Promotion::Rules::MinimumQuantity
89
88
  end
90
89
  ```
91
90
 
92
- ### Step 3: Create the Admin Partial
93
-
94
- Create a form partial so admins can configure the rule's preferences. The partial name must match the rule class name in underscore format:
91
+ Registered rules are discoverable at `/api/v3/admin/promotion_rules/types` together with their preference schema, and the dashboard's promotion editor is built on that endpoint — your rule appears there with a generated preferences form, no UI work needed.
95
92
 
96
- ```erb app/views/spree/admin/promotions/rules/_minimum_quantity.html.erb
97
- <div class="row mb-3">
98
- <%= f.spree_number_field :preferred_quantity, label: Spree.t(:minimum_quantity) %>
99
- </div>
100
- ```
93
+ ### Step 3: Add Translations
101
94
 
102
- ### Step 4: Add Translations
95
+ The rule's display name and description in the dashboard come from your locale file:
103
96
 
104
97
  ```yaml config/locales/en.yml
105
98
  en:
106
99
  spree:
107
- minimum_quantity: Minimum Quantity
108
100
  promotion_rule_types:
109
101
  minimum_quantity:
110
102
  name: Minimum Quantity
111
103
  description: Order must contain at least X items
112
104
  ```
113
105
 
114
- ### Step 5: Restart and Test
115
-
116
- After restarting your application, the new rule will be available in **Admin > Promotions** when adding rules to a promotion.
117
-
118
106
  ### Example: Rule with `actionable?`
119
107
 
120
- When your rule targets specific line items (not the whole order), implement `actionable?` so that actions like `CreateItemAdjustments` only discount matching items:
108
+ When your rule targets specific line items (not the whole order), implement `actionable?` so that item-level actions only discount matching items:
121
109
 
122
110
  ```ruby app/models/spree/promotion/rules/brand.rb
123
111
  module Spree
@@ -127,7 +115,7 @@ module Spree
127
115
  preference :brand_names, :array, default: []
128
116
 
129
117
  def applicable?(promotable)
130
- promotable.is_a?(Spree::Order)
118
+ promotable.is_a?(Spree::Order) || promotable.is_a?(Spree::Cart)
131
119
  end
132
120
 
133
121
  def eligible?(order, options = {})
@@ -153,13 +141,17 @@ end
153
141
 
154
142
  ## Custom Promotion Actions
155
143
 
156
- Actions define what happens when a promotion is applied. Most actions create [adjustments](../core-concepts/adjustments.md) on orders or line items.
144
+ Actions define what happens when a promotion is applied. A discount action doesn't write rows itself it declares **where** its discount belongs and **how much** it is, and Spree's promotion engine does the rest: running the competition between promotions, clamping amounts so nothing goes below zero, writing the winning Discount rows, and removing stale ones on every recalculation.
157
145
 
158
- ### Step 1: Create the Action Class
146
+ ### Discount Action (with Calculator)
159
147
 
160
- #### Discount Action (with Calculator)
148
+ Three declarations make a discount action:
161
149
 
162
- For actions that create monetary adjustments, include `Spree::CalculatedAdjustments` and `Spree::AdjustmentSource`:
150
+ | Method | Description |
151
+ |--------|-------------|
152
+ | `discount_scope` | Where the discount belongs: `:line_item` (per matching item), `:fulfillment` (per delivery), or `:order` (whole order, distributed proportionally across items) |
153
+ | `compute_amount(adjustable)` | The discount for one adjustable — a line item, fulfillment, or the order, matching your scope. Return a **negative** amount, or zero for "no discount". |
154
+ | `perform(options = {})` | Called when the promotion is activated. For discount actions, delegate to `apply_via_adjuster(options)`. |
163
155
 
164
156
  ```ruby app/models/spree/promotion/actions/tiered_discount.rb
165
157
  module Spree
@@ -167,15 +159,15 @@ module Spree
167
159
  module Actions
168
160
  class TieredDiscount < Spree::PromotionAction
169
161
  include Spree::CalculatedAdjustments
170
- include Spree::AdjustmentSource
171
162
 
172
163
  before_validation -> { self.calculator ||= Calculator::FlatRate.new }
173
164
 
174
- def perform(options = {})
175
- order = options[:order]
176
- return false unless order.present?
165
+ def discount_scope
166
+ :order
167
+ end
177
168
 
178
- create_unique_adjustment(order, order)
169
+ def perform(options = {})
170
+ apply_via_adjuster(options)
179
171
  end
180
172
 
181
173
  def compute_amount(order)
@@ -186,8 +178,7 @@ module Spree
186
178
  else 0
187
179
  end
188
180
 
189
- # Must return negative amount for discounts
190
- # Cap at order total to prevent negative orders
181
+ # Negative for discounts, capped at the order total
191
182
  [discount, order.item_total].min * -1
192
183
  end
193
184
  end
@@ -196,9 +187,13 @@ module Spree
196
187
  end
197
188
  ```
198
189
 
199
- #### Non-Discount Action
190
+ With `discount_scope :order`, the winning amount is shared out proportionally across the line items — you never handle the distribution yourself. With `:line_item`, `compute_amount` is called once per line item, and only items passing your rules' `actionable?` receive rows.
191
+
192
+ > **NOTE:** There is nothing to clean up either. If the promotion stops being eligible — the cart shrinks below the threshold, the code is removed — the next recalculation deletes its rows. If your action loses to a better promotion, its candidate simply isn't written that round, and it competes again on the next one.
200
193
 
201
- For actions that don't create adjustments (e.g., awarding points, sending notifications):
194
+ ### Non-Discount Action
195
+
196
+ For actions that don't create discounts (awarding points, sending notifications), implement `perform` alone:
202
197
 
203
198
  ```ruby app/models/spree/promotion/actions/add_loyalty_points.rb
204
199
  module Spree
@@ -220,45 +215,9 @@ module Spree
220
215
  end
221
216
  ```
222
217
 
223
- #### Key Methods to Implement
224
-
225
- | Method | Required | Description |
226
- |--------|----------|-------------|
227
- | `perform(options = {})` | Yes | Called when the promotion is activated. `options` includes `:order` and `:promotion`. Return `true` if the action was applied. |
228
- | `compute_amount(adjustable)` | For discount actions | Return the adjustment amount (negative for discounts). Cap at the adjustable's total to prevent negative amounts. |
229
- | `revert(options = {})` | No | Called when a promotion is deactivated. Use to undo side effects (e.g., remove added line items). |
230
-
231
- #### Available Helper Methods
232
-
233
- When you include `Spree::AdjustmentSource`, you get:
234
-
235
- ```ruby
236
- # Create a single adjustment (e.g., on the order)
237
- create_unique_adjustment(order, adjustable)
238
-
239
- # Create adjustments on multiple items (e.g., all line items)
240
- create_unique_adjustments(order, order.line_items)
241
-
242
- # With a filter block (e.g., only actionable line items)
243
- create_unique_adjustments(order, order.line_items) do |line_item|
244
- promotion.line_item_actionable?(order, line_item)
245
- end
246
- ```
247
-
248
- When you include `Spree::CalculatedAdjustments`, you get:
249
-
250
- ```ruby
251
- # Delegate to the calculator
252
- compute(adjustable) # calls calculator.compute(adjustable)
253
-
254
- # Set calculator by class name
255
- self.calculator_type = 'Spree::Calculator::FlatRate'
256
-
257
- # List available calculators for this action type
258
- self.class.calculators
259
- ```
218
+ `perform` receives `:order` and `:promotion` in its options and should return `true` if the action was applied. Optionally implement `revert(options = {})` to undo side effects when the promotion is deactivated.
260
219
 
261
- ### Step 2: Register the Action
220
+ ### Register and Translate
262
221
 
263
222
  ```ruby config/initializers/spree.rb
264
223
  Rails.application.config.after_initialize do
@@ -266,8 +225,6 @@ Rails.application.config.after_initialize do
266
225
  end
267
226
  ```
268
227
 
269
- ### Step 3: Add Translations
270
-
271
228
  ```yaml config/locales/en.yml
272
229
  en:
273
230
  spree:
@@ -277,39 +234,43 @@ en:
277
234
  description: Different discount amounts based on order total tiers
278
235
  ```
279
236
 
280
- ### Step 4: Restart and Test
281
-
282
- After restarting, the new action will be available in **Admin > Promotions** when adding actions to a promotion.
283
-
284
- ## How Rules and Actions Work Together
285
-
286
- Understanding how Spree evaluates promotions helps you build better custom rules and actions:
287
-
288
- ```mermaid
289
- flowchart TD
290
- A[Order Updated] --> B[PromotionHandler::Cart]
291
- B --> C{For each promotion}
292
- C --> D{Check dates & usage}
293
- D -->|expired/exceeded| E[Deactivate]
294
- D -->|valid| F{Evaluate rules}
295
- F -->|match_policy: all| G[ALL rules must pass]
296
- F -->|match_policy: any| H[ANY rule must pass]
297
- G -->|eligible| I[Run actions]
298
- H -->|eligible| I
299
- G -->|ineligible| E
300
- H -->|ineligible| E
301
- I --> J[action.perform for each action]
302
- J --> K[Create adjustments]
303
- K --> L[Best promotion wins]
237
+ Like rules, registered actions surface automatically in the dashboard's promotion editor via `/api/v3/admin/promotion_actions/types`.
238
+
239
+ ## Custom Adjusters
240
+
241
+ Not every charge or reduction is a promotion. A gift wrap fee, a payment surcharge, or loyalty pricing has no rules, no coupon codes, and no competition — it just needs to be on the order whenever it applies. That's an **adjuster**: a class invoked on every recalculation that owns a family of [Fee or Discount](../core-concepts/taxes-discounts-fees.md) rows.
242
+
243
+ ```ruby app/models/my_app/adjusters/gift_wrap.rb
244
+ module MyApp
245
+ module Adjusters
246
+ class GiftWrap < Spree::Adjusters::Base
247
+ def update
248
+ if order.gift_wrap?
249
+ order.fees.find_or_initialize_by(kind: 'gift_wrap').update!(
250
+ amount: 5.99, label: 'Gift wrapping'
251
+ )
252
+ else
253
+ order.fees.where(kind: 'gift_wrap').destroy_all
254
+ end
255
+ end
256
+ end
257
+ end
258
+ end
259
+ ```
260
+
261
+ ```ruby config/initializers/spree.rb
262
+ Rails.application.config.after_initialize do
263
+ Spree.adjusters << MyApp::Adjusters::GiftWrap
264
+ end
304
265
  ```
305
266
 
306
- **Key points:**
307
- - `match_policy: 'all'` means every rule must return `eligible? == true`
308
- - `match_policy: 'any'` means at least one rule must return `eligible? == true`
309
- - For item-level actions (`CreateItemAdjustments`), `actionable?(line_item)` on each rule filters which line items get the discount
310
- - When multiple promotions compete, Spree picks the best one (largest discount) and marks others as ineligible
267
+ The contract is one method: `update`. It runs on every recalculation, so it must be idempotent — write the rows that should exist, remove the ones that shouldn't (that's why the example uses `find_or_initialize_by` keyed by `kind` rather than `create`). The `order` it receives is the cart during checkout and the order after placement, and rows attach to it either way.
268
+
269
+ There's no totals bookkeeping to do: after all adjusters run, Spree re-sums the typed rows into the order totals and the tax provider estimates tax on the result — a fee you write here gets taxed in the same pass, like any other fee.
270
+
271
+ Custom discounts work the same way, writing `order.discounts` rows with your own `kind` (e.g. `'loyalty'`). Use a kind other than `'promotion'` — promotion rows belong to the promotion engine, which removes any it didn't write itself.
311
272
 
312
- ## Testing Custom Rules and Actions
273
+ ## Testing
313
274
 
314
275
  ```ruby spec/models/spree/promotion/rules/minimum_quantity_spec.rb
315
276
  require 'spec_helper'
@@ -367,7 +328,7 @@ end
367
328
 
368
329
  ## Related Documentation
369
330
 
370
- - [Promotions](../core-concepts/promotions.md) - Promotion architecture and built-in rules/actions
371
- - [Calculators](../core-concepts/calculators.md) - Available calculator types for promotion actions
372
- - [Adjustments](../core-concepts/adjustments.md) - How adjustments work on orders and line items
373
- - [Events](../core-concepts/events.md) - Subscribe to promotion events
331
+ - [Promotions](../core-concepts/promotions.md) promotion architecture and built-in rules and actions
332
+ - [Taxes, Discounts & Fees](../core-concepts/taxes-discounts-fees.md) the typed rows actions and adjusters write
333
+ - [Calculators](../core-concepts/calculators.md) available calculator types for promotion actions
334
+ - [Events](../core-concepts/events.md) subscribe to promotion events
@@ -9,7 +9,7 @@ Spree's reporting system is designed for extension. Each report is a pair of cla
9
9
 
10
10
  This guide walks you through building a custom report from scratch, including advanced patterns for SQL aggregations and multi-vendor support.
11
11
 
12
- Before starting, make sure you understand [how the reporting system works](../core-concepts/reports.md).
12
+ Before starting, make sure you understand [how the reporting system works](/v5/developer/core-concepts/reports).
13
13
 
14
14
  ## Creating a Custom Report
15
15
 
@@ -383,5 +383,5 @@ end
383
383
 
384
384
  ## Related Documentation
385
385
 
386
- - [Reports](../core-concepts/reports.md) - Report architecture and built-in reports
386
+ - [Reports](/v5/developer/core-concepts/reports) - Report architecture and built-in reports
387
387
  - [Events](../core-concepts/events.md) - How report generation uses the events system
@@ -5,11 +5,11 @@ description: Step-by-step guide to extending Spree's stock splitter chain — br
5
5
 
6
6
  ## Overview
7
7
 
8
- When [Order Routing](../core-concepts/shipments.md#order-routing) picks one or more stock locations to fulfill an order, each location's allocation is then run through a chain of **splitters**. Each splitter looks at the packages produced so far and decides whether to break them further along its own axis.
8
+ When [Order Routing](../core-concepts/fulfillments.md#order-routing) picks one or more stock locations to fulfill an order, each location's allocation is then run through a chain of **splitters**. Each splitter looks at the packages produced so far and decides whether to break them further along its own axis.
9
9
 
10
10
  Spree ships with four splitters out of the box (`ShippingCategory`, `Backordered`, `Digital`, `Weight`). You add your own when you need a *physical* separation that isn't expressed by any of the existing ones — refrigerated SKUs that can't share a box with ambient ones, hazmat goods that need their own carrier label, gift-wrap items that ship from a separate processing room, and so on.
11
11
 
12
- Before starting, make sure you understand [how splitting works in Spree](../core-concepts/shipments.md#stock-splitters) and the [order routing](../core-concepts/shipments.md#order-routing) layer that runs before splitters.
12
+ Before starting, make sure you understand [how splitting works in Spree](../core-concepts/fulfillments.md#stock-splitters) and the [order routing](../core-concepts/fulfillments.md#order-routing) layer that runs before splitters.
13
13
 
14
14
  | If the answer is "yes" | Pick |
15
15
  |---|---|
@@ -170,7 +170,7 @@ end
170
170
 
171
171
  ### Step 4 (optional): Add a Variant-side Predicate
172
172
 
173
- The example above relies on `variant.refrigerated?`. In a real plugin you'd back that with a [Custom Field](../core-concepts/custom-fields.md) on `Spree::Variant` — say a boolean metafield with key `refrigerated` — and define the predicate as a thin reader:
173
+ The example above relies on `variant.refrigerated?`. In a real plugin you'd back that with a [Custom Field](../core-concepts/metafields.md) on `Spree::Variant` — say a boolean metafield with key `refrigerated` — and define the predicate as a thin reader:
174
174
 
175
175
  ```ruby app/models/spree/variant_decorator.rb
176
176
  Spree::Variant.class_eval do
@@ -208,6 +208,6 @@ Practical implications:
208
208
 
209
209
  ## Next Steps
210
210
 
211
- - [Shipments — Stock Splitters](../core-concepts/shipments.md#stock-splitters) — Concept overview and built-in splitter list
211
+ - [Shipments — Stock Splitters](../core-concepts/fulfillments.md#stock-splitters) — Concept overview and built-in splitter list
212
212
  - [Build Custom Order Routing](custom-order-routing.md) — The other layer of split-shipment customization
213
- - [Custom Fields](../core-concepts/custom-fields.md) — Tag variants with the data your splitter reads
213
+ - [Custom Fields](../core-concepts/metafields.md) — Tag variants with the data your splitter reads
@@ -0,0 +1,20 @@
1
+ ---
2
+ title: Sell Digital Products
3
+ description: Digital goods in Spree — a digital delivery profile, instant fulfillment without a shipping address, and delivering files or entitlements after payment.
4
+ ---
5
+
6
+ Digital products skip the warehouse: no stock to reserve, no address to collect, no carrier to call. In Spree 6 that behavior comes from the product's delivery profile — assign a digital profile and checkout, fulfillment and the customer experience adapt.
7
+
8
+ ## What you will build
9
+
10
+ - **Digital delivery profile** — products that require no shipping address and complete checkout without a delivery step.
11
+ - **Instant fulfillment** — the digital fulfillment provider fulfills on payment rather than on dispatch.
12
+ - **Delivery of the goods** — attaching files or granting entitlements, and notifying the customer.
13
+ - **Mixed carts** — physical and digital items in one order, split into separate fulfillments automatically.
14
+
15
+ ## Outline
16
+
17
+ 1. Create a digital delivery profile and assign products
18
+ 2. Configure the digital fulfillment provider
19
+ 3. Deliver files or entitlements on fulfillment
20
+ 4. Handle mixed physical and digital orders
@@ -20,7 +20,7 @@ Each tenant (store) can have its own isolated data and configuration, including:
20
20
 
21
21
  All data is fully isolated besides the staff users, which can manage multiple tenants. This allows you to create a SaaS platform where each tenant can have its own store with its own branding and configuration. Isolation works across admin dashboard and API.
22
22
 
23
- > **INFO:** If you need individual seller/supplier/vendor accounts but shared product listings under one site you should use [multi vendor recipe](../multi-vendor.md) instead.
23
+ > **INFO:** If you need individual seller/supplier/vendor accounts but shared product listings under one site you should use [multi vendor recipe](../how-to/build-a-marketplace.md) instead.
24
24
 
25
25
  ## Prerequisites
26
26
 
@@ -0,0 +1,14 @@
1
+ ---
2
+ title: Product Media & DAM
3
+ description: Media hosted in your DAM stays in your DAM — Spree stores the address, not a copy of the bytes, and renders it through the same pipeline as uploaded media.
4
+ ---
5
+
6
+ > **NOTE:** This page documents functionality landing in an upcoming Spree 6.0 release. The interfaces described here may change before final release.
7
+
8
+ If your media lives in a digital asset management system or a CDN you already operate, importing copies into Spree doubles your storage and guarantees drift. Externally hosted media makes the address the record: Spree stores a URL, renders it wherever uploaded media renders, and your DAM remains the source of truth for the asset itself.
9
+
10
+ ## What this covers
11
+
12
+ - Product images and videos referenced by URL instead of uploaded bytes
13
+ - Feeding media URLs through the same product feeds and imports as the rest of your catalog data
14
+ - How externally hosted media behaves in the dashboard, the APIs and the storefront alongside uploaded media
@@ -0,0 +1,31 @@
1
+ ---
2
+ title: Connect Your ERP
3
+ description: Keep your ERP as the source of truth for stock. Spree syncs inventory in, answers availability from its own snapshot, and asks your ERP live only at the moments that matter.
4
+ ---
5
+
6
+ > **NOTE:** This page documents functionality landing in an upcoming Spree 6.0 release. The interfaces described here may change before final release.
7
+
8
+ Your ERP owns inventory truth. Spree's job is to sell against it without keeping a copy that drifts — and without making every product page wait on an ERP round trip.
9
+
10
+ ## What syncs, what stays live
11
+
12
+ Stock levels sync into Spree and availability renders from Spree's own records. Live calls to the inventory provider happen only at decision moments: add-to-cart, taking a hold, completing checkout. Anonymous browsing never waits on your ERP.
13
+
14
+ ## The inventory provider
15
+
16
+ A provider answers stock questions with Spree's own stock record shape, so everything downstream — backorder limits, serializers, availability scopes — keeps working without knowing where the number came from. External answers are read-only and never written into the catalog. The default provider reads Spree's own stock records; connecting nothing changes nothing.
17
+
18
+ ## Feeding stock from your ERP
19
+
20
+ Two ways in, both writing stock movements rather than overwriting numbers — so you can always see why a figure changed:
21
+
22
+ - **Bulk stock endpoint** — batch updates addressed by Spree IDs or by the keys your ERP already uses.
23
+ - **CSV stock import** — file-based feeds through the standard import pipeline.
24
+
25
+ ## Addressing records by your keys
26
+
27
+ The same product is known to your ERP and your PIM under different keys. External references are store-scoped mappings that let any feed address a Spree record as `external:<system>:<id>` — a create that names a key Spree already knows updates that record instead of failing.
28
+
29
+ ## Checkout holds and failure policy
30
+
31
+ Local checkout holds reduce availability against externally reported stock, matched by location, so reservations count correctly. When your ERP is unreachable, inventory falls back to the local snapshot — selling stock that turns out to be missing is recoverable; blocking every checkout is not.
@@ -0,0 +1,25 @@
1
+ ---
2
+ title: Shipping & WMS
3
+ description: Connect your warehouse, 3PL or carrier stack through fulfillment providers — label purchase, tracking, and delivery confirmation flow through one contract.
4
+ ---
5
+
6
+ When an order ships, something outside Spree does the physical work — a warehouse management system, a 3PL, or a carrier API. Fulfillment providers are the contract between Spree's fulfillment lifecycle and that system.
7
+
8
+ ## What a fulfillment provider does
9
+
10
+ A provider is attached to a delivery method and owns the outbound side of shipping:
11
+
12
+ - **Label purchase** — providers that generate labels declare it and buy the label *before* the fulfillment is marked as shipped, so the customer email always carries tracking. Labels can also be purchased explicitly ahead of dispatch.
13
+ - **Carrier notification** — creating a fulfillment with the provider is idempotent; cancelling or resuming a fulfillment stands the carrier down or re-engages it.
14
+
15
+ ## Tracking is its own axis
16
+
17
+ Carrier truth — in transit, out for delivery, delivered — is tracked separately from Spree's fulfillment status. Tracking updates flow in from carrier webhooks and update the tracking status without touching the fulfillment lifecycle; confirmed delivery anchors the returns window.
18
+
19
+ ## The EasyPost reference provider
20
+
21
+ The `spree_easypost` gem ships in the monorepo as the reference implementation: live carrier rates, label purchase and refunds, and tracker webhooks, with credentials managed through a Spree integration.
22
+
23
+ ## Writing your own
24
+
25
+ A WMS or 3PL integration implements the same contract: receive the fulfillment, do the physical work, report tracking back. Rate quoting is a separate, smaller contract — see [custom delivery rates](../how-to/custom-delivery-rate-provider.md).
@@ -1,5 +1,5 @@
1
1
  ---
2
- title: Telemetry
2
+ title: Observability
3
3
  description: Distributed tracing with OpenTelemetry — one gem, standard OTEL_* environment variables, and every checkout becomes a trace from HTTP request to gateway call to webhook delivery.
4
4
  ---
5
5
 
@@ -0,0 +1,44 @@
1
+ ---
2
+ title: Spree Fits Your Architecture
3
+ description: Spree is the commerce layer in the middle of the systems you already run — ERP, PIM, DAM, WMS, PSP, tax engines, search and identity connect through provider contracts, not forks.
4
+ ---
5
+
6
+ Most mid-size and enterprise merchants do not start from zero. They run an ERP that owns stock, a PIM that owns product data, a DAM that owns media, a warehouse system that ships boxes, and a payment provider with years of history. Replacing all of that to adopt a commerce platform is not a migration — it is a rebuild nobody asked for.
7
+
8
+ Spree takes the opposite position: **it is the system of record for what the shopper sees, and your existing systems stay the system of record for what is true.** Content, prices and stock sync in and render from Spree's own copy; live calls to your systems happen only at decision moments — pricing a line, adding to cart, taking a stock hold, completing an order.
9
+
10
+ ```mermaid
11
+ flowchart LR
12
+ PIM[PIM] --> S
13
+ ERP[ERP] --> S
14
+ DAM[DAM] --> S
15
+ subgraph S[Spree — commerce layer]
16
+ API[Store & Admin APIs]
17
+ end
18
+ S --> WMS[WMS / carriers]
19
+ S --> PSP[PSP]
20
+ S --> TAX[Tax engine]
21
+ S --> IDP[Identity provider]
22
+ S --> APM[Observability / APM]
23
+ ```
24
+
25
+ ## The provider contracts
26
+
27
+ Every connection point is a documented contract with a default implementation that uses Spree's own data — a store that connects nothing works exactly as before.
28
+
29
+ | Your system | Spree contract | Guide |
30
+ |---|---|---|
31
+ | ERP (stock levels) | Inventory provider | [Connect your ERP](erp.md) |
32
+ | PIM (product data, prices) | Pricing provider + data feeds | [Connect your PIM](pim.md) |
33
+ | DAM (media) | Externally hosted media | [Product media & DAM](dam.md) |
34
+ | WMS / 3PL / carriers | Fulfillment provider | [Shipping & WMS](fulfillment.md) |
35
+ | Carrier rate shopping | Delivery rate provider | [Custom delivery rates](../how-to/custom-delivery-rate-provider.md) |
36
+ | PSP | Payment gateway | [Custom payment method](../how-to/custom-payment-method.md) |
37
+ | Tax engine | Tax provider | [Avalara](../../integrations/tax/avalara.md) |
38
+ | Search | Search provider | [Custom search provider](../how-to/custom-search-provider.md) |
39
+ | Identity provider (SSO) | OpenID Connect | [Identity & SSO](sso.md) |
40
+ | APM / observability | OpenTelemetry | [Observability](observability.md) |
41
+
42
+ ## Credentials live in one place
43
+
44
+ Provider credentials are managed through Spree integrations — one admin surface for connecting external services, with secrets stored as masked preferences and connections verified before activation. Provider gems ship an integration class; there are no per-provider credential screens or environment-variable contracts for per-store services.
@@ -0,0 +1,25 @@
1
+ ---
2
+ title: Connect Your PIM
3
+ description: Keep product data and pricing in your PIM. Spree syncs content in, renders from its own copy, and can price live from an external source at cart decision moments.
4
+ ---
5
+
6
+ > **NOTE:** This page documents functionality landing in an upcoming Spree 6.0 release. The interfaces described here may change before final release.
7
+
8
+ Your PIM owns product content — names, descriptions, attributes, associations, prices. Spree renders the shopper-facing copy and asks your systems live only where a stale answer would cost money.
9
+
10
+ ## Product data feeds
11
+
12
+ Products, variants and prices flow in through the Admin API and CSV imports. Feeds address records by the keys your PIM already uses via external references (`external:<system>:<id>`), so an integration never has to store Spree IDs to update what it created.
13
+
14
+ ## The pricing provider
15
+
16
+ For merchants whose prices are computed elsewhere — contract pricing from an ERP, a pricing service, a PIM-driven price feed — a pricing provider answers price questions at cart decision moments with Spree's own price record shape. Tax-inclusive restatement and serializers keep working without knowing the number's origin.
17
+
18
+ Two properties of the contract matter for operations:
19
+
20
+ - **A provider declares a cache lifetime** and can decline anonymous browsing contexts, sending catalog pages straight to the local price resolver.
21
+ - **Pricing refuses on failure** rather than falling back — charging the wrong price is a dispute, not a retry.
22
+
23
+ ## Price imports
24
+
25
+ Price lists and plain price updates load through CSV imports and the Admin API, writing history rather than silently overwriting — the audit trail shows what changed and when.
@@ -0,0 +1,20 @@
1
+ ---
2
+ title: Identity & SSO
3
+ description: Single sign-on with OpenID Connect is built into Spree — connect your existing identity provider for staff and customer authentication without custom code.
4
+ ---
5
+
6
+ Enterprises already have an identity provider. Staff accounts live in it, security policy is enforced through it, and a commerce platform that demands its own password database is a liability. Spree 6 ships OpenID Connect support in core.
7
+
8
+ ## Staff single sign-on
9
+
10
+ Connect your identity provider so back-office users sign in to the dashboard through your existing SSO — sessions, MFA and offboarding stay governed by the system your security team already operates.
11
+
12
+ ## Customer authentication
13
+
14
+ Storefront customer authentication supports the same standards, so a merchant running a customer identity platform can keep it as the source of truth for shopper accounts.
15
+
16
+ ## Bring your identity provider
17
+
18
+ Any OpenID Connect issuer works. This guide will walk through connecting the common ones and the configuration each needs.
19
+
20
+ For wiring a custom or non-OIDC JWT issuer into the Store and Admin APIs directly, see [integrating a third-party identity provider](../how-to/custom-api-authentication.md).
@@ -66,5 +66,5 @@ The SDK transforms `filter`/`sort` into the Ransack-flavored query string the Ad
66
66
  ## See also
67
67
 
68
68
  - The [Store SDK extending guide](../extending.md) covers the same pattern for storefront endpoints — the API is identical, only the base path (`/api/v3/store`) and auth header differ.
69
- - The [API](../../tutorial/api.md) and [SDK](../../tutorial/sdk.md) tutorials walk through creating new Spree endpoints end-to-end (Rails controller, serializer, TypeScript types). Everything they show for the Store API applies equivalently for the Admin API.
69
+ - The [API](../../tutorial/admin-api.md) and [SDK](../../tutorial/storefront.md) tutorials walk through creating new Spree endpoints end-to-end (Rails controller, serializer, TypeScript types). Everything they show for the Store API applies equivalently for the Admin API.
70
70
  - For building dashboard UI on top of your custom endpoint, see the [Dashboard](../../dashboard/overview.md) section — start with [Customization Quickstart](../../dashboard/customization/quickstart.md) for in-app changes or [Plugin Overview](../../dashboard/plugins/overview.md) for redistributable packages.
@@ -66,4 +66,4 @@ const page: PaginatedResponse<Order> = await client.orders.list()
66
66
  - [Admin API Reference](../../../api-reference/admin-api/introduction.md) — The underlying REST API the SDK wraps — every endpoint, parameter, and response shape.
67
67
 
68
68
 
69
- > **TIP:** Adding your own resources? The [`spree generate api_resource`](../../tutorial/api.md) generator scaffolds Admin API endpoints (model, serializers, controllers, specs) in one command, and the [extending guide](../extending.md) shows how to call custom endpoints from the SDK.
69
+ > **TIP:** Adding your own resources? The [`spree generate api_resource`](../../tutorial/admin-api.md) generator scaffolds Admin API endpoints (model, serializers, controllers, specs) in one command, and the [extending guide](../extending.md) shows how to call custom endpoints from the SDK.
@@ -125,4 +125,4 @@ await client.customFields('Spree::Product', 'prod_xxx').list()
125
125
 
126
126
  ## Adding your own resources
127
127
 
128
- The resource map grows with your store. The [`spree generate api_resource`](../../tutorial/api.md) generator scaffolds new Admin API endpoints (model, serializers, controllers, routes, specs) in one command — then call them from the SDK with a [custom fetch or raw request](../extending.md), or generate a typed client for them.
128
+ The resource map grows with your store. The [`spree generate api_resource`](../../tutorial/admin-api.md) generator scaffolds new Admin API endpoints (model, serializers, controllers, routes, specs) in one command — then call them from the SDK with a [custom fetch or raw request](../extending.md), or generate a typed client for them.
@@ -30,4 +30,4 @@ const nike = await client.request<Brand>('GET', '/brands/nike')
30
30
 
31
31
  `client.request` uses the same auth headers, retry logic, and locale/currency defaults as `client.products.list()` or any other built-in resource.
32
32
 
33
- For a complete walkthrough — creating the API endpoints on the backend, defining TypeScript types, filtering, and expanding associations — see the [API](../tutorial/api.md) and [SDK](../tutorial/sdk.md) tutorials.
33
+ For a complete walkthrough — creating the API endpoints on the backend, defining TypeScript types, filtering, and expanding associations — see the [API](../tutorial/admin-api.md) and [SDK](../tutorial/storefront.md) tutorials.
@@ -0,0 +1,13 @@
1
+ ---
2
+ title: Admin API
3
+ description: Add a ProductFaq endpoint to the Admin API — resource controller conventions, an admin serializer, and a permission catalog entry.
4
+ ---
5
+
6
+ Staff manage FAQs through the Admin API — the same API the dashboard, integrations and scripts use. This chapter adds the endpoint following the conventions every Spree resource follows, which is what makes the dashboard plugin in the next chapter nearly free.
7
+
8
+ ## What this chapter covers
9
+
10
+ - The resource controller: full CRUD from one small controller declaring its model, serializer and permitted attributes
11
+ - The admin serializer and prefixed IDs
12
+ - Registering the resource in the permission catalog so roles and API key scopes cover it
13
+ - Exercising the endpoint with the Admin CLI