@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.
- package/dist/api-reference/store.yaml +48 -48
- package/dist/developer/cli/quickstart.md +1 -1
- package/dist/developer/contributing/creating-an-extension.md +2 -2
- package/dist/developer/core-concepts/addresses.md +3 -3
- package/dist/developer/core-concepts/architecture.md +4 -4
- package/dist/developer/core-concepts/calculators.md +2 -2
- package/dist/developer/core-concepts/carts.md +207 -0
- package/dist/developer/core-concepts/channels.md +3 -3
- package/dist/developer/core-concepts/customers.md +1 -1
- package/dist/developer/core-concepts/events.md +1 -1
- package/dist/developer/core-concepts/fulfillments.md +278 -0
- package/dist/developer/core-concepts/imports-exports.md +6 -18
- package/dist/developer/core-concepts/inventory.md +3 -3
- package/dist/developer/core-concepts/media.md +1 -1
- package/dist/developer/core-concepts/metafields.md +1 -1
- package/dist/developer/core-concepts/orders.md +92 -339
- package/dist/developer/core-concepts/payments.md +1 -1
- package/dist/developer/core-concepts/promotions.md +68 -195
- package/dist/developer/core-concepts/returns-exchanges-claims.md +173 -0
- package/dist/developer/core-concepts/store-credits-gift-cards.md +16 -18
- package/dist/developer/core-concepts/stores.md +1 -1
- package/dist/developer/core-concepts/taxes-discounts-fees.md +199 -0
- package/dist/developer/core-concepts/taxes.md +3 -3
- package/dist/developer/create-spree-app/quickstart.md +1 -1
- package/dist/developer/customization/api.md +36 -11
- package/dist/developer/customization/configuration.md +207 -37
- package/dist/developer/customization/decorators.md +8 -8
- package/dist/developer/customization/permissions.md +34 -229
- package/dist/developer/customization/quickstart.md +159 -108
- package/dist/developer/customization/validations.md +1 -1
- package/dist/developer/dashboard/overview.md +1 -1
- package/dist/developer/how-to/build-a-b2b-store.md +21 -0
- package/dist/developer/how-to/build-a-marketplace.md +25 -0
- package/dist/developer/how-to/custom-api-authentication.md +1 -1
- package/dist/developer/how-to/custom-delivery-rate-provider.md +242 -0
- package/dist/developer/how-to/custom-document-numbers.md +154 -0
- package/dist/developer/how-to/custom-order-routing.md +1 -1
- package/dist/developer/how-to/custom-payment-method.md +1 -1
- package/dist/developer/how-to/custom-promotion.md +76 -115
- package/dist/developer/how-to/custom-report.md +2 -2
- package/dist/developer/how-to/custom-stock-splitter.md +5 -5
- package/dist/developer/how-to/sell-digital-products.md +20 -0
- package/dist/developer/multi-tenant/quickstart.md +1 -1
- package/dist/developer/providers/dam.md +14 -0
- package/dist/developer/providers/erp.md +31 -0
- package/dist/developer/providers/fulfillment.md +25 -0
- package/dist/developer/{deployment/telemetry.md → providers/observability.md} +1 -1
- package/dist/developer/providers/overview.md +44 -0
- package/dist/developer/providers/pim.md +25 -0
- package/dist/developer/providers/sso.md +20 -0
- package/dist/developer/sdk/admin/extending.md +1 -1
- package/dist/developer/sdk/admin/quickstart.md +1 -1
- package/dist/developer/sdk/admin/resources.md +1 -1
- package/dist/developer/sdk/extending.md +1 -1
- package/dist/developer/tutorial/admin-api.md +13 -0
- package/dist/developer/tutorial/dashboard-plugin.md +14 -0
- package/dist/developer/tutorial/events.md +7 -161
- package/dist/developer/tutorial/introduction.md +15 -35
- package/dist/developer/tutorial/model.md +7 -98
- package/dist/developer/tutorial/store-api.md +13 -0
- package/dist/developer/tutorial/storefront.md +12 -0
- package/dist/developer/tutorial/testing.md +7 -711
- package/dist/developer/upgrades/5.6-to-6.0.md +2 -2
- package/package.json +1 -1
- package/dist/developer/admin/admin.md +0 -214
- package/dist/developer/admin/authentication.md +0 -59
- package/dist/developer/admin/components.md +0 -711
- package/dist/developer/admin/custom-css.md +0 -256
- package/dist/developer/admin/custom-javascript.md +0 -116
- package/dist/developer/admin/extending-ui.md +0 -1839
- package/dist/developer/admin/form-builder.md +0 -444
- package/dist/developer/admin/helper-methods.md +0 -531
- package/dist/developer/admin/navigation.md +0 -805
- package/dist/developer/admin/tables.md +0 -490
- package/dist/developer/advanced/adding_spree_to_rails_app.md +0 -92
- package/dist/developer/core-concepts/adjustments.md +0 -113
- package/dist/developer/core-concepts/reports.md +0 -208
- package/dist/developer/core-concepts/shipments.md +0 -307
- package/dist/developer/core-concepts/users.md +0 -303
- package/dist/developer/customization/authentication.md +0 -100
- package/dist/developer/customization/checkout.md +0 -202
- package/dist/developer/customization/emails.md +0 -18
- package/dist/developer/customization/routes.md +0 -24
- package/dist/developer/multi-vendor/installation.md +0 -61
- package/dist/developer/multi-vendor/quickstart.md +0 -17
- package/dist/developer/tutorial/admin.md +0 -206
- package/dist/developer/tutorial/api.md +0 -606
- package/dist/developer/tutorial/extending-models.md +0 -393
- package/dist/developer/tutorial/sdk.md +0 -170
- package/dist/developer/upgrades/2.0-to-2.1.md +0 -46
- package/dist/developer/upgrades/2.1-to-2.2.md +0 -59
- package/dist/developer/upgrades/2.2-to-2.3.md +0 -44
- package/dist/developer/upgrades/2.3-to-2.4.md +0 -42
- package/dist/developer/upgrades/3.0-to-3.1.md +0 -47
- package/dist/developer/upgrades/3.1-to-3.2.md +0 -34
- package/dist/developer/upgrades/3.2-to-3.3.md +0 -70
- package/dist/developer/upgrades/3.3-to-3.4.md +0 -36
- package/dist/developer/upgrades/3.4-to-3.5.md +0 -44
- package/dist/developer/upgrades/3.5-to-3.6.md +0 -40
- package/dist/developer/upgrades/3.6-to-3.7.md +0 -62
- package/dist/developer/upgrades/3.7-to-4.0.md +0 -152
- package/dist/developer/upgrades/4.0-to-4.1.md +0 -92
- package/dist/developer/upgrades/4.1-to-4.2.md +0 -109
- package/dist/developer/upgrades/4.10-to-5.0.md +0 -131
- package/dist/developer/upgrades/4.2-to-4.3.md +0 -100
- package/dist/developer/upgrades/4.3-to-4.4.md +0 -125
- package/dist/developer/upgrades/4.4-to-4.5.md +0 -94
- package/dist/developer/upgrades/4.5-to-4.6.md +0 -119
- package/dist/developer/upgrades/4.6-to-4.7.md +0 -39
- package/dist/developer/upgrades/4.8-to-4.9.md +0 -24
- package/dist/developer/upgrades/4.9-to-4.10.md +0 -24
- package/dist/developer/upgrades/4.x-to-4.8.md +0 -52
- package/dist/developer/upgrades/5.0-to-5.1.md +0 -28
- package/dist/developer/upgrades/5.1-to-5.2.md +0 -131
- package/dist/developer/upgrades/5.2-to-5.3.md +0 -338
- package/dist/developer/upgrades/5.3-to-5.4.md +0 -277
- package/dist/developer/upgrades/5.4-to-5.5.md +0 -301
- 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
|
|
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
|
|
8
|
+
Spree's promotion system has three extension points:
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
###
|
|
146
|
+
### Discount Action (with Calculator)
|
|
159
147
|
|
|
160
|
-
|
|
148
|
+
Three declarations make a discount action:
|
|
161
149
|
|
|
162
|
-
|
|
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
|
|
175
|
-
|
|
176
|
-
|
|
165
|
+
def discount_scope
|
|
166
|
+
:order
|
|
167
|
+
end
|
|
177
168
|
|
|
178
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
|
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)
|
|
371
|
-
- [
|
|
372
|
-
- [
|
|
373
|
-
- [Events](../core-concepts/events.md)
|
|
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](
|
|
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](
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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](../
|
|
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).
|
|
@@ -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/
|
|
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/
|
|
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
|