@spree/docs 0.1.177 → 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 (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 +207 -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
@@ -5,23 +5,18 @@ description: Build percentage and fixed-amount discounts, free shipping, BOGO of
5
5
 
6
6
  ## Overview
7
7
 
8
- Promotions within Spree are used to provide discounts to orders, offer free shipping, or add items at no extra cost (e.g., free gifts). The promotion system is highly flexible, allowing you to create complex promotional campaigns based on various conditions.
9
-
10
- ### Promotion Model Diagram
8
+ Promotions give money off an order a percentage off, free shipping, or free items added to the cart. A promotion is a campaign definition: **rules** decide when it applies, **actions** decide what it does. When a promotion applies, its actions write [Discount](taxes-discounts-fees.md#discounts) rows on the order.
11
9
 
12
10
  ```mermaid
13
11
  erDiagram
14
12
  Promotion {
15
13
  string name
16
- string description
17
14
  string kind
18
15
  string code
19
- string path
20
16
  datetime starts_at
21
17
  datetime expires_at
22
18
  integer usage_limit
23
19
  string match_policy
24
- boolean advertise
25
20
  }
26
21
 
27
22
  PromotionRule {
@@ -31,7 +26,6 @@ erDiagram
31
26
 
32
27
  PromotionAction {
33
28
  string type
34
- integer position
35
29
  }
36
30
 
37
31
  CouponCode {
@@ -39,48 +33,33 @@ erDiagram
39
33
  string state
40
34
  }
41
35
 
42
- OrderPromotion {
43
- integer order_id
44
- integer promotion_id
45
- }
46
-
47
- Adjustment {
48
- decimal amount
36
+ Discount {
49
37
  string label
50
- boolean eligible
51
- string state
38
+ string kind
39
+ string code
40
+ string amount
52
41
  }
53
42
 
54
43
  Promotion ||--o{ PromotionRule : "has many"
55
44
  Promotion ||--o{ PromotionAction : "has many"
56
45
  Promotion ||--o{ CouponCode : "has many"
57
- Promotion ||--o{ OrderPromotion : "has many"
58
- Promotion }o--o{ Store : "available in"
59
- PromotionRule }o--o{ Product : "applies to"
60
- PromotionRule }o--o{ Taxon : "applies to"
61
- PromotionRule }o--o{ User : "applies to"
62
- PromotionAction ||--o{ Adjustment : "creates"
63
- PromotionAction ||--|| Calculator : "has one"
64
- Order ||--o{ OrderPromotion : "has many"
65
- CouponCode }o--o| Order : "used by"
46
+ PromotionAction ||--o{ Discount : "writes"
47
+ Order ||--o{ Discount : "has many"
66
48
  ```
67
49
 
68
- **Key relationships:**
69
- - **Promotion** defines the campaign with rules and actions
70
- - **Promotion Rules** determine eligibility conditions (FirstOrder, ItemTotal, Taxon, etc.)
71
- - **Promotion Actions** define what happens when applied (discount, free shipping, free items)
72
- - **Coupon Codes** track usage for code-based promotions
73
- - **[Adjustments](adjustments.md)** are created on orders/line items to apply discounts
74
-
75
50
  Promotions can be activated in two ways:
76
51
 
77
- - **Automatic promotions** - Applied automatically when eligibility rules are met (e.g., free shipping on orders over $50)
78
- - **Coupon code promotions** - Applied when a customer enters a valid code during checkout
52
+ - **Automatic promotions** applied on their own when the rules are met (e.g. free shipping on orders over $50)
53
+ - **Coupon code promotions** applied when a customer enters a valid code during checkout
54
+
55
+ ### How discounts are recorded
56
+
57
+ Each applied promotion becomes one or more Discount rows on the order — on a specific line item or on a fulfillment, never a loose adjustment. Each row keeps a copy of the coupon code and the configured value ("10% off"), so the order still tells the whole story even if the promotion is deleted later.
79
58
 
80
- Promotions consist of two main components:
59
+ Two things follow from this design:
81
60
 
82
- - **Rules** - Conditions that must be met for the promotion to apply
83
- - **Actions** - What happens when the promotion is applied (discount, free shipping, etc.)
61
+ - **Only winners are stored.** When several promotions compete for the same item, Spree picks the one that saves the customer the most and writes only that one. Losing candidates are recomputed on every cart change, so a promotion that loses today can win tomorrow without any bookkeeping.
62
+ - **A discount on the whole order is shared out across the items.** If a customer later returns one item, Spree knows exactly how much of the discount belonged to it.
84
63
 
85
64
  ## Promotion Attributes
86
65
 
@@ -89,16 +68,15 @@ Promotions consist of two main components:
89
68
  | `name` | The name of the promotion | Summer Sale |
90
69
  | `description` | Brief description (max 255 chars) | 20% off all summer items |
91
70
  | `kind` | Type: `coupon_code` or `automatic` | `automatic` |
92
- | `starts_at` | When the promotion becomes active | 2025-06-01 00:00:00 |
93
- | `expires_at` | When the promotion expires | 2025-09-01 23:59:59 |
71
+ | `starts_at` | When the promotion becomes active | 2026-06-01 00:00:00 |
72
+ | `expires_at` | When the promotion expires | 2026-09-01 23:59:59 |
94
73
  | `usage_limit` | Max times the promotion can be used | 500 |
95
74
  | `match_policy` | How rules are evaluated: `all` or `any` | `all` |
96
75
  | `advertise` | Whether to display on storefront | `true` |
97
- | `path` | SEO-friendly URL path | `summer-sale` |
98
76
 
99
77
  ### Multi-Code Promotions
100
78
 
101
- For promotions that need unique codes per customer (e.g., influencer campaigns), Spree supports bulk code generation:
79
+ For promotions that need unique codes per customer (e.g. influencer campaigns), Spree supports bulk code generation:
102
80
 
103
81
  | Attribute | Description | Example |
104
82
  |-----------|-------------|---------|
@@ -106,158 +84,65 @@ For promotions that need unique codes per customer (e.g., influencer campaigns),
106
84
  | `number_of_codes` | How many codes to generate | 1000 |
107
85
  | `code_prefix` | Prefix for generated codes | `SUMMER` |
108
86
 
109
- Generated codes follow the pattern `{prefix}{random}`, e.g., `SUMMER22A0F62A230BD919`.
87
+ Generated codes follow the pattern `{prefix}{random}`, e.g. `SUMMER22A0F62A230BD919`.
110
88
 
111
89
  ## Rules
112
90
 
113
- Rules determine when a promotion is eligible. You can combine multiple rules and configure whether **all** rules must match or **any** rule is sufficient (via `match_policy`).
114
-
115
- ### FirstOrder
116
-
117
- Applies only to a customer's first order. Checks both user account and email address to prevent abuse.
118
-
119
- **Use case:** Welcome discount for new customers.
120
-
121
- ### ItemTotal
122
-
123
- Requires the order subtotal to meet minimum and/or maximum thresholds.
124
-
125
- **Configuration:**
126
- - `preferred_amount_min` - Minimum order amount (default: 100.00)
127
- - `preferred_operator_min` - Comparison operator: `gt` (greater than) or `gte` (greater than or equal)
128
- - `preferred_amount_max` - Maximum order amount (optional)
129
- - `preferred_operator_max` - Comparison operator: `lt` (less than) or `lte` (less than or equal)
130
-
131
- **Use case:** "Free shipping on orders over $50", "10% off orders between $100-$500".
132
-
133
- ### Product
134
-
135
- Requires specific products to be in the order.
136
-
137
- **Configuration:**
138
- - `preferred_match_policy`:
139
- - `any` - At least one of the specified products
140
- - `all` - All specified products must be present
141
- - `none` - None of the specified products (exclusion)
142
-
143
- **Use case:** "Buy Product A and get 20% off", "Discount excludes sale items".
144
-
145
- ### Taxon
146
-
147
- Requires products from specific categories (taxons) to be in the order.
148
-
149
- **Configuration:**
150
- - `preferred_match_policy`: `any` or `all`
151
-
152
- The rule automatically includes products in child taxons when a parent taxon is selected.
153
-
154
- **Use case:** "20% off Electronics", "Buy from Shoes category get 10% off".
155
-
156
- ### User
157
-
158
- Limits the promotion to specific customer accounts.
159
-
160
- **Use case:** Employee discounts, VIP customer promotions.
161
-
162
- ### UserLoggedIn
163
-
164
- Requires the customer to be logged in with an account.
165
-
166
- **Use case:** Member-only discounts.
167
-
168
- ### OneUsePerUser
169
-
170
- Ensures each customer can only use the promotion once.
171
-
172
- **Use case:** One-time welcome offers, limited redemption campaigns.
173
-
174
- ### Country
175
-
176
- Limits the promotion to orders shipping to a specific country.
177
-
178
- **Configuration:**
179
- - `preferred_country_id` or `preferred_country_iso` (ISO 2-letter code)
180
-
181
- **Use case:** Regional promotions, country-specific campaigns.
182
-
183
- ### Currency
184
-
185
- Limits the promotion to orders in a specific currency.
186
-
187
- **Configuration:**
188
- - `preferred_currency` - Currency code (e.g., "USD", "EUR")
189
-
190
- **Use case:** Currency-specific pricing strategies.
191
-
192
- ### OptionValue
193
-
194
- Requires products with specific option values (e.g., size, color) to be in the order.
195
-
196
- **Configuration:**
197
- - `preferred_eligible_values` - Array of `Spree::OptionValueVariant` IDs (the option-value↔variant join records the rule matches against the order's variants), not variant IDs
198
-
199
- **Use case:** "10% off all red items", "Discount on size XL".
200
-
201
- ### CustomerGroup
202
-
203
- Limits the promotion to customers belonging to specific customer groups.
204
-
205
- **Configuration:**
206
- - `preferred_customer_group_ids` - Array of customer group IDs
207
-
208
- Customer groups are managed in Admin > Customers > Customer Groups and allow you to segment customers for targeted promotions.
209
-
210
- **Use case:** VIP programs, loyalty tiers, wholesale pricing, B2B customer segments.
91
+ Rules decide when a promotion is eligible. You can combine multiple rules and configure whether **all** must match or **any** is enough (via `match_policy`).
92
+
93
+ | Rule | Applies when |
94
+ |---|---|
95
+ | **FirstOrder** | It's the customer's first order (checked by account and email) |
96
+ | **ItemTotal** | The order subtotal is within configured minimum/maximum thresholds |
97
+ | **Product** | Specific products are in the order (`any` / `all` / `none` match policy) |
98
+ | **Category** | Products from specific categories are in the order — child categories count |
99
+ | **OptionValue** | Products with specific option values (size, color) are in the order |
100
+ | **User** | The customer is one of the listed accounts |
101
+ | **UserLoggedIn** | The customer is logged in |
102
+ | **OneUsePerUser** | The customer hasn't used this promotion before |
103
+ | **CustomerGroup** | The customer belongs to one of the configured customer groups |
104
+ | **Country** | The order ships to a specific country |
105
+ | **Currency** | The order is in a specific currency |
106
+ | **Channel** | The order was placed through one of the configured [channels](channels.md) |
107
+ | **Market** | The order belongs to one of the configured [markets](markets.md) |
108
+
109
+ You can also [build your own rules](../how-to/custom-promotion.md) for business-specific conditions.
211
110
 
212
111
  ## Actions
213
112
 
214
- Actions define what happens when a promotion is applied.
113
+ Actions define what happens when a promotion applies.
215
114
 
216
115
  ### CreateAdjustment
217
116
 
218
- Creates a discount on the entire order total.
117
+ A discount on the whole order. The amount is distributed proportionally across the line items — there is no single order-level row, so per-item reporting and partial returns always know their share.
219
118
 
220
- **Default calculator:** `FlatPercentItemTotal` (percentage off order)
221
-
222
- **Available calculators:**
223
- - `FlatPercentItemTotal` - Percentage off the order total
224
- - `FlatRate` - Fixed amount off
225
- - `FlexiRate` - First item at one rate, additional items at another
226
- - `TieredFlatRate` - Different rates based on order total tiers
227
- - `TieredPercent` - Different percentages based on order total tiers
119
+ **Default calculator:** `FlatPercentItemTotal` (percentage off the order). Also available: `FlatRate`, `FlexiRate`, `TieredFlatRate`, `TieredPercent`.
228
120
 
229
121
  **Use case:** "10% off your order", "$20 off orders over $100".
230
122
 
231
123
  ### CreateItemAdjustments
232
124
 
233
- Creates discounts on individual line items. Only applies to items that match the promotion rules (e.g., items in a specific category).
234
-
235
- **Default calculator:** `PercentOnLineItem` (percentage off each item)
125
+ A discount on individual line items. Only items that match the promotion's rules receive it if a Category rule says "Electronics", only electronics are discounted.
236
126
 
237
- The action respects rule actionability - if a Taxon rule specifies "Electronics", only electronics items receive the discount.
127
+ **Default calculator:** `PercentOnLineItem` (percentage off each item).
238
128
 
239
129
  **Use case:** "15% off shoes", "Buy 2+ shirts get 10% off each".
240
130
 
241
131
  ### FreeShipping
242
132
 
243
- Makes all shipments free by creating negative adjustments equal to shipping costs.
133
+ Writes a discount on each fulfillment covering its delivery cost. The row is kept even when the cost is zero — its presence is what marks the order as having free shipping.
244
134
 
245
135
  **Use case:** "Free shipping on orders over $75", "Free shipping with code FREESHIP".
246
136
 
247
137
  ### CreateLineItems
248
138
 
249
- Automatically adds specified products to the cart when the promotion is eligible.
250
-
251
- **Configuration:**
252
- - Map of variant IDs to quantities to add
253
-
254
- The action checks stock availability before adding items. Items are not automatically removed if eligibility is lost - customers must remove them manually.
139
+ Adds specified products to the cart when the promotion is eligible, checking stock first. Items are not removed automatically if eligibility is lost — customers remove them manually.
255
140
 
256
- **Use case:** "Free gift with purchase", "Buy 2 get 1 free", "Spend $100 get free sample".
141
+ **Use case:** "Free gift with purchase", "Spend $100 get a free sample".
257
142
 
258
143
  ## Managing Promotions
259
144
 
260
- Create and manage promotions via the [Admin API](../../api-reference/admin-api/introduction.md). Rules and actions can be supplied inline on create — each is a `{ type, preferences }` draft using the rule/action types described above:
145
+ Create and manage promotions via the [Admin API](../../api-reference/admin-api/introduction.md). Rules and actions can be supplied inline on create — each is a `{ type, preferences }` draft using the rule and action types described above:
261
146
 
262
147
 
263
148
  ```typescript Admin SDK
@@ -310,26 +195,18 @@ spree api patch /promotions/promo_xxx -d '{"expires_at": "2026-09-01T00:00:00Z"}
310
195
  ```
311
196
 
312
197
 
313
- ## Coupon Codes
314
-
315
- Coupon codes track promotion usage and can be single-use or multi-use.
316
-
317
- ### Single Code Promotions
318
-
319
- Set the `code` attribute directly on the promotion. The `usage_limit` controls how many times it can be redeemed.
198
+ The available rule and action types — including any custom ones you register — are discoverable at `/api/v3/admin/promotion_rules/types` and `/api/v3/admin/promotion_actions/types`, together with their preference schemas. The dashboard's promotion editor is built on these endpoints, so custom types show up there without any UI work.
320
199
 
321
- ### Multi-Code Promotions
200
+ > **NOTE:** Deleting a promotion doesn't disturb the orders that used it. Their discount rows stay, keeping the code and value they were created with — only the link back to the promotion is cleared.
322
201
 
323
- For bulk-generated codes, each code tracks its usage state:
202
+ ## Coupon Codes
324
203
 
325
- | Attribute | Description | Example |
326
- |-----------|-------------|---------|
327
- | `code` | The unique code | `SUMMER22A0F62A230BD919` |
328
- | `state` | Usage state | `unused`, `used` |
204
+ Coupon codes track promotion usage and can be single-use or multi-use.
329
205
 
330
- ## Applying Coupon Codes via Store API
206
+ - **Single code** set the `code` attribute on the promotion; `usage_limit` controls how many times it can be redeemed.
207
+ - **Multi-code** — bulk-generated codes each track their own state (`unused` / `used`).
331
208
 
332
- Customers [apply coupon codes during checkout](../sdk/store/cart-checkout.md) via the Store API:
209
+ Customers apply codes on the cart via the Store API:
333
210
 
334
211
 
335
212
  ```typescript Store SDK
@@ -354,26 +231,22 @@ curl -X POST 'https://api.mystore.com/api/v3/store/carts/cart_abc123/discount_co
354
231
  ```
355
232
 
356
233
 
357
- Automatic promotions are applied without any customer actionthey are evaluated on every order update.
234
+ The returned cart already carries the updated discounts and totals. If the cart doesn't qualify yet say the code needs a $100 minimum and the cart holds $80 — the code stays on the cart and the discount activates on its own the moment the cart qualifies.
358
235
 
359
- ## Promotion Flow
236
+ Automatic promotions need no customer action at all — they are evaluated on every cart change.
360
237
 
361
- How promotions are evaluated and applied:
238
+ ## Promotion Flow
362
239
 
363
- 1. **Order Update** — When an order is updated, the system checks all eligible promotions
364
- 2. **Eligibility Check** — For each promotion:
365
- - Check if within `starts_at` and `expires_at` range
366
- - Check if under `usage_limit`
367
- - Evaluate all rules based on `match_policy` (all or any)
368
- 3. **Action Execution** — If eligible, each action is applied
369
- 4. **Tracking** — Applied promotions are recorded on the order
370
- 5. **Re-evaluation** — Eligibility is re-checked on every order update; adjustments from ineligible promotions are removed
240
+ 1. **Cart change** — every change to a cart (item added, address entered, code applied) triggers a recalculation
241
+ 2. **Eligibility** — each connected promotion is checked: active dates, usage limits, then rules per `match_policy`
242
+ 3. **Competition** candidate discounts are computed for every eligible promotion; per item, per fulfillment, and order-wide, the largest saving wins
243
+ 4. **Persistence** winning discounts are written as rows; anything stale from the previous pass is removed; tax is then estimated on the discounted amounts
244
+ 5. **Placement** once the order is placed, its discount rows are frozen; usage counts are recorded against the promotion
371
245
 
372
246
  ## Related Documentation
373
247
 
374
- - [Build Custom Promotion Rules & Actions](../how-to/custom-promotion.md) - Step-by-step guide to creating custom rules and actions
375
- - [Calculators](calculators.md) - Learn about promotion calculators
376
- - [Orders](orders.md) - Understanding order processing
377
- - [Customization Quickstart](../customization/quickstart.md) - Overview of customization options
378
- - [Admin SDK Resources](../sdk/admin/resources.md) - The `client.promotions.*` resource-client pattern used in the examples above
379
- - [Spree CLI Admin API](../cli/admin-api.md) - The `spree api` command reference for the CLI examples above
248
+ - [Taxes, Discounts & Fees](taxes-discounts-fees.md) the Discount rows promotions write, and manual discounts
249
+ - [Build Custom Promotion Rules & Actions](../how-to/custom-promotion.md) — step-by-step guide to custom rules, actions and adjusters
250
+ - [Calculators](calculators.md) promotion calculators
251
+ - [Carts](carts.md) the cart lifecycle promotions act on
252
+ - [Admin SDK Resources](../sdk/admin/resources.md) the `client.promotions.*` resource-client pattern used above
@@ -0,0 +1,173 @@
1
+ ---
2
+ title: Returns, Exchanges & Claims
3
+ description: How Spree models what happens after a customer receives an order — three separate records for money back, different items, and something gone wrong.
4
+ ---
5
+
6
+ ## Overview
7
+
8
+ Three things can happen after a customer receives an order, and Spree models each on its own:
9
+
10
+ | Record | What happened | Outcome |
11
+ |---|---|---|
12
+ | **Return** | Customer sends items back | Money back |
13
+ | **Exchange** | Customer sends items back | Different items |
14
+ | **Claim** | Item arrived damaged, wrong, or never arrived | Refund, replacement, or both — usually without asking for the goods back |
15
+
16
+ Keeping them separate matters most for claims. Without a record for "it arrived smashed", merchants end up either creating a manual order or opening a return they immediately mark received — which records the wrong thing and makes damage reporting impossible.
17
+
18
+ ```mermaid
19
+ erDiagram
20
+ Order ||--o{ Return : "has many"
21
+ Order ||--o{ Exchange : "has many"
22
+ Order ||--o{ Claim : "has many"
23
+ Return ||--o{ ReturnLineItem : "has many"
24
+ Return }o--|| StockLocation : "received at"
25
+
26
+ Return {
27
+ string number
28
+ string status
29
+ datetime received_at
30
+ }
31
+ Claim {
32
+ string number
33
+ string status
34
+ string resolution
35
+ }
36
+ ```
37
+
38
+ ## Statuses
39
+
40
+ | Record | Statuses |
41
+ |---|---|
42
+ | **Return** | `requested` → `approved` → `received` → `refunded`, or `canceled` |
43
+ | **Exchange** | `requested` → `approved` → `received` → `fulfilled`, or `canceled` |
44
+ | **Claim** | `open` → `approved` → `resolved`, or `denied` / `canceled` |
45
+
46
+ Each step is its own API call, because each one needs information the last one didn't have — what actually turned up, how much to refund, what to send instead.
47
+
48
+ ## Customer self-service
49
+
50
+ Customers can open a return or a claim on their own order and follow its progress. Approving, receiving and refunding stay with the merchant.
51
+
52
+
53
+ ```typescript Store SDK
54
+ // Request a return — items refer to units that shipped, not cart line items
55
+ const returnRequest = await client.orders.returns.create('or_xxx', {
56
+ items: [{ fulfillment_item_id: 'fi_xxx', quantity: 1 }],
57
+ reason_id: 'rsn_xxx',
58
+ memo: 'Too small',
59
+ })
60
+
61
+ // Report a problem
62
+ const claim = await client.orders.claims.create('or_xxx', {
63
+ items: [{ line_item_id: 'li_xxx', quantity: 1, description: 'Arrived cracked' }],
64
+ reason_id: 'clr_xxx',
65
+ })
66
+
67
+ // Follow progress
68
+ const returns = await client.orders.returns.list('or_xxx')
69
+ ```
70
+
71
+ ```bash cURL
72
+ curl -X POST 'https://api.mystore.com/api/v3/store/orders/or_xxx/returns' \
73
+ -H 'X-Spree-API-Key: pk_xxx' \
74
+ -H 'X-Spree-Token: order_token_xxx' \
75
+ -H 'Content-Type: application/json' \
76
+ -d '{ "items": [{ "fulfillment_item_id": "fi_xxx", "quantity": 1 }],
77
+ "memo": "Too small" }'
78
+ ```
79
+
80
+
81
+ Claim types are `damaged`, `missing`, `wrong_item` and `other` out of the box, and a store can add its own.
82
+
83
+ ## Processing a return
84
+
85
+
86
+ ```typescript Admin SDK
87
+ // Approve
88
+ await adminClient.orders.returns.approve('or_xxx', 'ret_xxx')
89
+
90
+ // Record what actually arrived
91
+ await adminClient.orders.returns.receive('or_xxx', 'ret_xxx', {
92
+ items: [
93
+ { return_line_item_id: 'rli_1', quantity: 2, resellable: true },
94
+ { return_line_item_id: 'rli_2', quantity: 1, resellable: false },
95
+ ],
96
+ })
97
+
98
+ // Refund
99
+ await adminClient.orders.returns.refund('or_xxx', 'ret_xxx', {
100
+ amount: '24.99',
101
+ refund_method: 'original_payment',
102
+ })
103
+ ```
104
+
105
+ ```bash cURL
106
+ curl -X PATCH 'https://api.mystore.com/api/v3/admin/orders/or_xxx/returns/ret_xxx/receive' \
107
+ -H 'X-Spree-API-Key: sk_xxx' \
108
+ -H 'Content-Type: application/json' \
109
+ -d '{ "items": [{ "return_line_item_id": "rli_1", "quantity": 2, "resellable": true }] }'
110
+ ```
111
+
112
+
113
+ Receiving takes the quantities the warehouse actually counted, because partial and damaged returns are normal rather than exceptional. A customer says three items are coming; two arrive; one of those can't be sold again. Only resellable goods go back into stock.
114
+
115
+ Leave `items` out to receive everything as requested. Refunds default to whatever the return is still owed, and can go back to the original payment method or to store credit.
116
+
117
+ ## Resolving a claim
118
+
119
+ What to do about a claim is decided when you resolve it, not when the customer opens it — merchants usually decide once they've seen the photos.
120
+
121
+ ```typescript Admin SDK
122
+ await adminClient.orders.claims.approve('or_xxx', 'clm_xxx')
123
+
124
+ await adminClient.orders.claims.resolve('or_xxx', 'clm_xxx', {
125
+ resolution: 'refund_and_replacement', // or 'refund', 'replacement'
126
+ refund_method: 'store_credit',
127
+ replacement_line_item_ids: ['li_xxx'],
128
+ })
129
+ ```
130
+
131
+ A replacement creates a new [fulfillment](fulfillments.md) on the original order, so the customer doesn't have to place a second one.
132
+
133
+ ## Exchanges
134
+
135
+ An exchange works like a return, but ends by sending different items instead of refunding:
136
+
137
+ ```typescript Admin SDK
138
+ await adminClient.orders.exchanges.approve('or_xxx', 'exch_xxx')
139
+ await adminClient.orders.exchanges.receive('or_xxx', 'exch_xxx')
140
+ await adminClient.orders.exchanges.fulfill('or_xxx', 'exch_xxx')
141
+ ```
142
+
143
+ ## Reporting
144
+
145
+ Because each one is its own record, you can query them directly:
146
+
147
+ ```typescript Admin SDK
148
+ // Everything awaiting action
149
+ const pending = await adminClient.returns.list({ filter: { status_eq: 'approved' } })
150
+
151
+ // Claims opened this month
152
+ const claims = await adminClient.claims.list({
153
+ filter: { created_at_gt: '2026-08-01' },
154
+ })
155
+ ```
156
+
157
+ ## Return policy
158
+
159
+ Spree ships no built-in return window. Whether a customer may open a return is store policy, and it's checked when the return is created — so you can hold customers to a 30-day window while letting staff make an exception for a good customer, and vary the rule by market where local law requires it.
160
+
161
+ Set the return window per market, or express a more specific rule in your own application. See [Configuration](../customization/configuration.md) and [Services & Workflows](../customization/workflows.md).
162
+
163
+ ## Events
164
+
165
+ Each step publishes an [event](events.md) — `return.received`, `return.refunded`, `exchange.fulfilled`, `claim.resolved` — which also reach [webhooks](webhooks.md).
166
+
167
+ Returns and claims also update the order's [payment status](orders.md#statuses), so a refunded order reflects it without any manual bookkeeping.
168
+
169
+ ## Related
170
+
171
+ - [Orders](orders.md) — payment status after refunds
172
+ - [Fulfillments](fulfillments.md) — replacement deliveries
173
+ - [Inventory](inventory.md) — putting returned goods back in stock
@@ -174,7 +174,7 @@ erDiagram
174
174
 
175
175
  GiftCard {
176
176
  string code
177
- string state
177
+ string status
178
178
  decimal amount
179
179
  decimal amount_used
180
180
  decimal amount_authorized
@@ -199,28 +199,26 @@ erDiagram
199
199
  | `code` | Unique redemption code | `abc1234def` |
200
200
  | `amount` | Total gift card value | `50.00` |
201
201
  | `amount_used` | Amount already redeemed | `0.00` |
202
- | `state` | Current state | `active` |
202
+ | `status` | Current status | `active` |
203
203
  | `currency` | Currency code | `USD` |
204
204
  | `expires_at` | Optional expiration date | `2025-12-31` |
205
205
 
206
- ### Gift Card States
206
+ ### Gift Card Statuses
207
207
 
208
- ```mermaid
209
- stateDiagram-v2
210
- [*] --> active: Created
211
- active --> partially_redeemed: Partial redemption
212
- active --> redeemed: Full redemption
213
- active --> canceled: Admin cancels
214
- partially_redeemed --> redeemed: Remaining redeemed
215
- partially_redeemed --> partially_redeemed: Another partial redemption
216
- ```
208
+ | Status | Description | How it gets there |
209
+ |-------|-------------|-------------------|
210
+ | `active` | Available for redemption | The status a card is created with |
211
+ | `partially_redeemed` | Some value has been redeemed | `Spree::GiftCards::Redeem`, when a balance remains |
212
+ | `redeemed` | Fully redeemed | `Spree::GiftCards::Redeem`, when nothing is left |
213
+ | `canceled` | Voided, and no longer spendable | `Spree::GiftCards::Cancel` |
217
214
 
218
- | State | Description |
219
- |-------|-------------|
220
- | `active` | Available for redemption |
221
- | `partially_redeemed` | Some value has been redeemed |
222
- | `redeemed` | Fully redeemed |
223
- | `canceled` | Canceled by admin |
215
+ Redemption does not need the caller to choose: `Spree::GiftCards::Redeem`
216
+ looks at what is left on the card and marks it partially or fully redeemed
217
+ accordingly. A card can be partially redeemed more than once, and each spend
218
+ publishes its own event.
219
+
220
+ Cancelling is refused once a card has been spent against, so cancellation can
221
+ never take back value a customer has already used.
224
222
 
225
223
  > **NOTE:** Gift cards can also be `expired` if `expires_at` date has passed and the card hasn't been fully redeemed.
226
224
 
@@ -74,7 +74,7 @@ Each store owns its own resources. Products, orders, channels, markets, and taxo
74
74
  | [**Products**](products.md) | A product belongs to one store. Its visibility across channels is controlled by [publications](channels.md#publishing-products-on-channels). |
75
75
  | [**Categories**](products.md#categories) | A category belongs to one store |
76
76
  | [**Payment Methods**](payments.md) | A payment method belongs to one store |
77
- | [**Shipping Methods**](shipments.md) | A shipping method belongs to one store |
77
+ | [**Shipping Methods**](fulfillments.md) | A shipping method belongs to one store |
78
78
  | [**Promotions**](promotions.md) | A promotion belongs to one store |
79
79
 
80
80
  ## Running Multiple Storefronts