@spree/docs 0.1.177 → 0.1.179
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +208 -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
|
@@ -5,23 +5,18 @@ description: Build percentage and fixed-amount discounts, free shipping, BOGO of
|
|
|
5
5
|
|
|
6
6
|
## Overview
|
|
7
7
|
|
|
8
|
-
Promotions
|
|
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
|
-
|
|
43
|
-
integer order_id
|
|
44
|
-
integer promotion_id
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
Adjustment {
|
|
48
|
-
decimal amount
|
|
36
|
+
Discount {
|
|
49
37
|
string label
|
|
50
|
-
|
|
51
|
-
string
|
|
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
|
-
|
|
58
|
-
|
|
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**
|
|
78
|
-
- **Coupon code promotions**
|
|
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
|
-
|
|
59
|
+
Two things follow from this design:
|
|
81
60
|
|
|
82
|
-
- **
|
|
83
|
-
- **
|
|
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 |
|
|
93
|
-
| `expires_at` | When the promotion expires |
|
|
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
|
|
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
|
|
87
|
+
Generated codes follow the pattern `{prefix}{random}`, e.g. `SUMMER22A0F62A230BD919`.
|
|
110
88
|
|
|
111
89
|
## Rules
|
|
112
90
|
|
|
113
|
-
Rules
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
**
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
**
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
113
|
+
Actions define what happens when a promotion applies.
|
|
215
114
|
|
|
216
115
|
### CreateAdjustment
|
|
217
116
|
|
|
218
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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", "
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
202
|
+
## Coupon Codes
|
|
324
203
|
|
|
325
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
236
|
+
Automatic promotions need no customer action at all — they are evaluated on every cart change.
|
|
360
237
|
|
|
361
|
-
|
|
238
|
+
## Promotion Flow
|
|
362
239
|
|
|
363
|
-
1. **
|
|
364
|
-
2. **Eligibility
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
- [
|
|
375
|
-
- [
|
|
376
|
-
- [
|
|
377
|
-
- [
|
|
378
|
-
- [Admin SDK Resources](../sdk/admin/resources.md)
|
|
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
|
|
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
|
-
| `
|
|
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
|
|
206
|
+
### Gift Card Statuses
|
|
207
207
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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**](
|
|
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
|