@spree/docs 0.1.181 → 0.1.183

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 (31) hide show
  1. package/dist/api-reference/seller-api/errors.md +1 -1
  2. package/dist/api-reference/store.yaml +78 -91
  3. package/dist/developer/core-concepts/addresses.md +106 -198
  4. package/dist/developer/core-concepts/architecture.md +97 -126
  5. package/dist/developer/core-concepts/calculators.md +75 -252
  6. package/dist/developer/core-concepts/carts.md +1 -1
  7. package/dist/developer/core-concepts/channels.md +0 -4
  8. package/dist/developer/core-concepts/companies-and-catalogs.md +1 -1
  9. package/dist/developer/core-concepts/customers.md +0 -3
  10. package/dist/developer/core-concepts/discounts.md +133 -0
  11. package/dist/developer/core-concepts/events.md +83 -576
  12. package/dist/developer/core-concepts/fees.md +144 -0
  13. package/dist/developer/core-concepts/imports-exports.md +105 -679
  14. package/dist/developer/core-concepts/inventory.md +114 -248
  15. package/dist/developer/core-concepts/markets.md +9 -12
  16. package/dist/developer/core-concepts/media.md +9 -11
  17. package/dist/developer/core-concepts/metafields.md +123 -200
  18. package/dist/developer/core-concepts/order-totals.md +110 -0
  19. package/dist/developer/core-concepts/orders.md +1 -1
  20. package/dist/developer/core-concepts/payments.md +11 -14
  21. package/dist/developer/core-concepts/pricing.md +11 -13
  22. package/dist/developer/core-concepts/products.md +173 -19
  23. package/dist/developer/core-concepts/promotions.md +12 -11
  24. package/dist/developer/core-concepts/search-filtering.md +2 -4
  25. package/dist/developer/core-concepts/store-credits-gift-cards.md +0 -3
  26. package/dist/developer/core-concepts/taxes.md +125 -113
  27. package/dist/developer/core-concepts/translations.md +61 -68
  28. package/dist/developer/core-concepts/webhooks.md +25 -59
  29. package/dist/developer/how-to/custom-promotion.md +3 -3
  30. package/package.json +1 -1
  31. package/dist/developer/core-concepts/taxes-discounts-fees.md +0 -199
@@ -1,303 +1,126 @@
1
1
  ---
2
2
  title: Calculators
3
+ description: The small pieces of arithmetic behind delivery charges and promotion discounts — what each one does and how to choose between them.
3
4
  ---
4
5
 
5
6
  ## Overview
6
7
 
7
- ### Calculator Model Diagram
8
-
9
- ```mermaid
10
- erDiagram
11
- Calculator {
12
- string type
13
- string calculable_type
14
- text preferences
15
- }
16
-
17
- TaxRate {
18
- string name
19
- decimal amount
20
- boolean included_in_price
21
- }
22
-
23
- ShippingMethod {
24
- string name
25
- string display_on
26
- }
27
-
28
- PromotionAction {
29
- string type
30
- }
31
-
32
- Adjustment {
33
- decimal amount
34
- string label
35
- }
36
-
37
- TaxRate ||--|| Calculator : "has one"
38
- ShippingMethod ||--|| Calculator : "has one"
39
- PromotionAction ||--|| Calculator : "has one"
40
- TaxRate ||--o{ Adjustment : "creates"
41
- ShippingMethod ||--o{ ShippingRate : "calculates"
42
- PromotionAction ||--o{ Adjustment : "creates"
43
- ```
44
-
45
- **Key relationships:**
46
- - **Calculator** computes amounts for various features
47
- - Used by **[Tax Rates](taxes.md)** to calculate tax amounts
48
- - Used by **[Shipping Methods](fulfillments.md)** to calculate shipping costs
49
- - Used by **[Promotion Actions](promotions.md)** to calculate discounts
50
- - Calculators store [preferences (rates, percentages, etc.)](../customization/model-preferences.md) for their calculations
51
-
52
- Spree makes extensive use of the `Spree::Calculator` model and there are several subclasses provided to deal with various types of calculations flat rate, percentage discount, sales tax, VAT, etc. All calculators extend the `Spree::Calculator` class and must provide the following methods:
53
-
54
- ```ruby
55
- def self.description
56
- # Human readable description of the calculator
57
- end
58
-
59
- def compute(object=nil)
60
- # Returns the value after performing the required calculation
61
- end
62
- ```
63
-
64
- Calculators link to a `calculable` object, which are typically one of `Spree::ShippingMethod`, `Spree::TaxRate`, or `Spree::Promotion::Actions::CreateAdjustment`. These three classes use the `Spree::CalculatedAdjustments` module described below to provide an easy way to calculate adjustments for their objects.
65
-
66
- ## Available Calculators
67
-
68
- The following are descriptions of the currently available calculators in Spree. If you would like to add your own, please see the [Creating a New Calculator](#creating-a-new-calculator) section.
69
-
70
- ### Default Tax
71
-
72
- For information about this calculator, please read the [Taxes](taxes.md) guide.
73
-
74
- ### Flat Percent Per Item Total
75
-
76
- This calculator has one preference: `flat_percent` and can be set like this:
77
-
78
- ```ruby
79
- calculator.preferred_flat_percent = 10
80
- ```
81
-
82
- This calculator takes an order and calculates an amount using this calculation:
83
-
84
- ```ruby
85
- [item total] x [flat percentage]
86
- ```
87
-
88
- For example, if an order had an item total of `$31` and the calculator was configured to have a flat percent amount of `10`, the discount would be `$3.10`, because `$31 x 10% = $3.10`.
89
-
90
- ### Flat Rate
91
-
92
- This calculator can be used to provide a flat rate discount.
93
-
94
- This calculator has two preferences: `amount` and `currency`. These can be set like this:
95
-
96
- ```ruby
97
- calculator.preferred_amount = 10
98
- calculator.preferred_currency = "USD"
99
- ```
100
-
101
- The currency for this calculator is used to check to see if a shipping method is available for an order. If an order's currency does not match the shipping method's currency, then that shipping method will not be displayed on the frontend.
102
-
103
- This calculator can take any object and will return simply the preferred amount.
104
-
105
- ### Flexi Rate
106
-
107
- This calculator is typically used for promotional discounts when you want a specific discount for the first product, and then subsequent discounts for other products, up to a certain amount.
8
+ A calculator works out an amount. "Charge $5 per item." "Take 15% off." "Free over $50, otherwise $7."
108
9
 
109
- This calculator takes three preferences:
10
+ They exist because the rule and the number are different questions. A promotion decides *whether* a discount applies; a calculator decides *how much*. Keeping them apart means a merchant can change "10% off" to "$10 off" without touching the conditions that decide who qualifies.
110
11
 
111
- * `first_item`: The discounted price of the first items.
112
- * `additional_item`: The discounted price of subsequent items.
113
- * `max_items`: The maximum number of items this discount applies to.
12
+ Two things use calculators:
114
13
 
115
- The calculator computes based on this:
14
+ - **[Delivery methods](fulfillments.md)** what delivery costs
15
+ - **[Promotions](promotions.md)** — how big a discount is
116
16
 
117
- ```text
118
- [first item discount] + (([items_count*] - 1) x [additional item discount])
119
- ```
120
-
121
- * up to the `max_items`
122
-
123
- Thus, if you have ten items in your shopping cart, your `first_item` preference is set to `$10`, your `additional_items` preference is set to `$5`, and your `max_items` preference is set to `4`, the total discount would be `$25`:
124
-
125
- * `$10` for the first item
126
- * `$5` for each of the `3` subsequent items: `$5 \* 3 = $15`
127
- * `$0` for the remaining `6` items
128
-
129
- ### Per Item
130
-
131
- The Per Item calculator (`Spree::Calculator::Shipping::PerItem`) is a shipping calculator that charges a flat amount for every item in a shipment.
132
-
133
- This calculator takes two preferences:
134
-
135
- * `amount`: The flat amount charged per item.
136
- * `currency`: The currency for this calculator.
137
-
138
- It computes a flat rate per item by multiplying the `amount` preference by the total item quantity in the shipment package:
139
-
140
- ```text
141
- [amount] x [total item quantity in package]
142
- ```
143
-
144
- For example, with an `amount` of `5` and a package containing `3` items in total, the calculator computes an amount of `15` (`5 x 3`).
145
-
146
- ### Percent Per Item
147
-
148
- The Percent Per Item calculator (`Spree::Calculator::PercentOnLineItem`) applies a percentage discount to a single line item. It takes two preferences:
17
+ > **INFO:** Tax does **not** use calculators. Tax is worked out by a [tax provider](taxes.md), which can be Spree's own rate tables or an external service.
149
18
 
150
- * `percent`: The percentage to apply to the line item's amount.
151
- * `apply_only_on_full_priced_items`: When enabled, skips line items that are already on sale.
19
+ ## Choosing a calculator
152
20
 
153
- For each line item, the calculator computes `line item amount` x `percent` / `100`, capped at the line item's amount so a promotion adjustment never pushes the total negative.
21
+ Each calculator takes a few settings — the amount, the percentage, the threshold. A merchant picks one and fills in the settings when setting up a delivery method or a promotion.
154
22
 
155
- For example, a `$30` line item at `10%` yields a `$3` discount (`$30 x 10% = $3`).
23
+ ### For delivery charges
156
24
 
157
- ### Price Sack
25
+ | Calculator | What it charges | Settings |
26
+ |---|---|---|
27
+ | **Flat rate** | The same amount every time | `amount`, `currency` |
28
+ | **Per item** | An amount for each item in the parcel | `amount`, `currency` |
29
+ | **Percent of item total** | A percentage of what's in the parcel | `flat_percent` |
30
+ | **Price sack** | One amount above a threshold, another below — the usual "free delivery over $50" | `minimal_amount`, `discount_amount`, `normal_amount` |
31
+ | **Flexi rate** | A charge for the first item, less for each one after | `first_item`, `additional_item`, `max_items` |
32
+ | **Digital delivery** | Nothing, for downloads | — |
158
33
 
159
- The Price Sack calculator is useful for when you want to provide a discount for an order which is over a certain price. The calculator has four preferences:
34
+ ### For promotion discounts
160
35
 
161
- * `minimal_amount`: The minimum amount for the line items total to trigger the calculator.
162
- * `discount_amount`: The amount to discount from the order if the line items total is equal to or greater than the `minimal_amount`.
163
- * `normal_amount`: The amount to discount from the order if the line items total is less than the `minimal_amount`.
164
- * `currency`: The currency for this calculator. Defaults to the store currency
36
+ | Calculator | What it takes off | Settings |
37
+ |---|---|---|
38
+ | **Flat rate** | A fixed amount | `amount`, `currency` |
39
+ | **Percent of item total** | A percentage of the order | `flat_percent` |
40
+ | **Percent on line item** | A percentage of one item | `percent` |
41
+ | **Flexi rate** | A sliding amount by quantity | `first_item`, `additional_item`, `max_items` |
42
+ | **Tiered percent** | A percentage that grows with order value | tiers |
43
+ | **Tiered flat rate** | A fixed amount that grows with order value | tiers |
165
44
 
166
- Suppose you have a Price Sack calculator with a `minimal_amount` preference of `$50`, a `normal_amount` preference of `$2`, and a `discount_amount` of `$5`. An order with a line items total of `$60` would result in a discount of `$5` for the whole order. An order of `$20` would result in a discount of `$2`.
45
+ ## Worked examples
167
46
 
168
- ## Creating a New Calculator
47
+ <details>
48
+ <summary>Free delivery over $50, otherwise $7</summary>
169
49
 
170
- To create a new calculator for Spree, you need to do two things. The first is to inherit from the `Spree::Calculator` class and define `description` and `compute` methods on that class:
50
+ **Price sack**, with `minimal_amount: 50`, `discount_amount: 0`, `normal_amount: 7`.
171
51
 
172
- ```ruby
173
- class CustomCalculator < Spree::Calculator
174
- def self.description
175
- # Human readable description of the calculator
176
- end
52
+ A $60 basket pays nothing. A $20 basket pays $7.
177
53
 
178
- def compute(object=nil)
179
- # Returns the value after performing the required calculation
180
- end
181
- end
182
- ```
54
+ </details>
183
55
 
184
- If you are creating a new calculator for shipping methods, please be aware that you need to inherit from `Spree::ShippingCalculator` instead, and define a `compute_package` method:
56
+ <details>
57
+ <summary>$3 to ship the first item, $1 for each extra</summary>
185
58
 
186
- ```ruby
187
- class CustomCalculator < Spree::ShippingCalculator
188
- def self.description
189
- # Human readable description of the calculator
190
- end
59
+ **Flexi rate**, with `first_item: 3`, `additional_item: 1`.
191
60
 
192
- def compute_package(package)
193
- # Returns the value after performing the required calculation
194
- end
195
- end
196
- ```
61
+ Four items cost `$3 + (3 × $1)` = **$6**. Set `max_items` to stop charging beyond a point.
197
62
 
198
- The second thing is to register this calculator as a tax, shipping, or promotion adjustment calculator by calling code like this at the end of `config/initializers/spree.rb` inside your application `config` variable defined for brevity:
63
+ </details>
199
64
 
200
- **Spree 5.2+:**
65
+ <details>
66
+ <summary>10% off the order</summary>
201
67
 
202
- ```ruby config/initializers/spree.rb
203
- Rails.application.config.after_initialize do
204
- Spree.calculators.tax_rates << CustomCalculator
205
- Spree.calculators.shipping_methods << CustomCalculator
206
- Spree.calculators.promotion_actions_create_adjustments << CustomCalculator
207
- end
208
- ```
68
+ **Percent of item total**, with `flat_percent: 10`.
209
69
 
210
- **Spree 5.1 and below:**
70
+ A $31 order gets **$3.10** off.
211
71
 
212
- ```ruby config/initializers/spree.rb
213
- Rails.application.config.spree.calculators.tax_rates << CustomCalculator
214
- Rails.application.config.spree.calculators.shipping_methods << CustomCalculator
215
- Rails.application.config.spree.calculators.promotion_actions_create_adjustments << CustomCalculator
216
- ```
72
+ </details>
217
73
 
74
+ <details>
75
+ <summary>Spend more, save more</summary>
218
76
 
219
- For example if your calculator is placed in `app/models/spree/calculator/shipping/my_own_calculator.rb` you should call:
77
+ **Tiered percent** 5% over $100, 10% over $250, 15% over $500.
220
78
 
221
- **Spree 5.2+:**
79
+ The order total picks the tier. One promotion covers the whole ladder instead of three competing ones.
222
80
 
223
- ```ruby config/initializers/spree.rb
224
- Rails.application.config.after_initialize do
225
- Spree.calculators.shipping_methods << Spree::Calculator::Shipping::MyOwnCalculator
226
- end
227
- ```
81
+ </details>
228
82
 
229
- **Spree 5.1 and below:**
230
83
 
231
- ```ruby config/initializers/spree.rb
232
- Rails.application.config.spree.calculators.shipping_methods << Spree::Calculator::Shipping::MyOwnCalculator
233
- ```
84
+ ## Setting one up
234
85
 
86
+ Calculators are configured as part of the thing that uses them. The available calculators for each are discoverable, so a dashboard or a script can present the real list rather than a hardcoded one:
235
87
 
236
- ### Determining Availability
237
88
 
238
- By default, all shipping method calculators are available at all times. If you wish to make this dependent on something from the order, you can re-define the `available?` method inside your calculator:
89
+ ```typescript Admin SDK
90
+ // What can price a delivery method?
91
+ const calculators = await adminClient.deliveryMethods.calculators()
239
92
 
240
- ```ruby app/models/custom_calculator.rb
241
- class CustomCalculator < Spree::Calculator
242
- def available?(object)
243
- object.currency == "USD"
244
- end
245
- end
93
+ // Set up a delivery method with a flat rate
94
+ await adminClient.deliveryMethods.create({
95
+ name: 'Standard shipping',
96
+ delivery_zone_id: 'dz_xxx',
97
+ calculator_type: 'Spree::Calculator::Shipping::FlatRate',
98
+ calculator_attributes: {
99
+ preferences: { amount: '7.00', currency: 'USD' },
100
+ },
101
+ })
246
102
  ```
247
103
 
248
- ## Calculated Adjustments
249
-
250
- If you wish to use Spree's calculator functionality for your own application, you can include the `Spree::CalculatedAdjustments` module into a model of your choosing.
251
-
252
- ```ruby app/models/plan.rb
253
- class Plan < ActiveRecord::Base
254
- include Spree::CalculatedAdjustments
255
- end
104
+ ```bash CLI
105
+ spree api get /delivery_methods/calculators
256
106
  ```
257
107
 
258
- To have calculators available for this class, you will need to register them. `Spree.calculators` is a fixed-member struct (`SpreeCalculators`, defined in `spree/core/lib/spree/core/engine.rb`) that exposes only the four built-in buckets:
259
-
260
- * `shipping_methods`
261
- * `tax_rates`
262
- * `promotion_actions_create_adjustments`
263
- * `promotion_actions_create_item_adjustments`
264
-
265
- `Plan.calculators` internally calls `Spree.calculators.send(:plans)` (the tableized model name), so both registration and lookup raise `NoMethodError` until you extend the struct to add a matching `plans` member. Ruby structs cannot gain members at runtime, so this means redefining the `SpreeCalculators` struct (or reassigning `Spree.calculators` to an object that responds to `plans`) before you register anything onto it.
266
-
267
- Once the struct exposes a `plans` member you can register calculators:
268
108
 
269
- **Spree 5.2+:**
109
+ Each calculator declares which settings it takes, so a form can be built for it without knowing the calculator in advance — which is how the dashboard renders these.
270
110
 
271
- ```ruby config/initializers/spree.rb
272
- Rails.application.config.after_initialize do
273
- Spree.calculators.plans << CustomCalculator
274
- end
275
- ```
111
+ > **WARNING:** A calculator with an `amount` also has a **currency**. If it doesn't match the order's currency, the calculator contributes nothing rather than converting. Set up one calculator per currency you sell in, or a promotion will silently do nothing for some customers.
276
112
 
277
- **Spree 5.1 and below:**
113
+ ## Writing your own
278
114
 
279
- ```ruby config/initializers/spree.rb
280
- Rails.application.config.spree.calculators.plans << CustomCalculator
281
- ```
115
+ When none of the built-in calculators expresses your pricing — dimensional weight, a contract rate card, a rule your finance team invented — you can add one. A calculator is a small class with a name and a method that returns an amount, registered so it appears alongside the built-in options.
282
116
 
117
+ For delivery specifically, consider whether you want a calculator or a **delivery rate provider**. A calculator computes a number from what's in the parcel. A provider asks a carrier for real, live rates. If you want what UPS would actually charge today, that's a provider.
283
118
 
284
- Then you can access these calculators by calling this method:
285
-
286
- ```ruby
287
- Plan.calculators
288
- ```
289
-
290
- Using this method, you can then display the calculators as you please. Each object for this new class will need to have a calculator associated so that adjustments can be calculated on them.
291
-
292
- `Spree::CalculatedAdjustments` provides a `has_one :calculator` association (with `accepts_nested_attributes_for` and a presence validation), delegates `compute` to that calculator, exposes a `with_calculator` scope, `calculator_type` / `calculator_type=` accessors, and the `Plan.calculators` registry shown above. To work out what the calculator would compute an amount to be, call `compute` on an instance:
293
-
294
- ```ruby
295
- plan.compute(<calculable object>)
296
- ```
297
-
298
- The module does not define `create_adjustment`, `update_adjustment`, or `compute_amount`. If you also need to build adjustments, include `Spree::AdjustmentSource`, which adds `create_adjustment(order, adjustable, included = false)`. That method calls a `compute_amount` you define on the including model (as `Spree::TaxRate` and the `Spree::Promotion::Actions` classes do). There is no `update_adjustment` method in core.
119
+ See [Custom promotions](../how-to/custom-promotion.md) and [Providers](../providers/overview.md).
299
120
 
300
- ## Related Documentation
121
+ ## Related
301
122
 
302
- - [Adjustments](taxes-discounts-fees.md) — the records calculators compute amounts for.
303
- - [Dependencies](../customization/dependencies.md) — swap out calculators via the Dependencies system.
123
+ - [Fulfillments](fulfillments.md) — delivery methods and rates
124
+ - [Promotions](promotions.md) — the rules that decide when a discount applies
125
+ - [Discounts](discounts.md) — the rows a promotion produces
126
+ - [Taxes](taxes.md) — worked out by providers, not calculators
@@ -136,7 +136,7 @@ This is what lets you design your own checkout. Spree tells you what's missing;
136
136
  shipping_address: {
137
137
  first_name: 'John', last_name: 'Doe',
138
138
  address1: '123 Main St', city: 'Los Angeles',
139
- country_code: 'US', state_abbr: 'CA', postal_code: '90001',
139
+ country_code: 'US', state_code: 'CA', postal_code: '90001',
140
140
  },
141
141
  })
142
142
  ```
@@ -3,9 +3,6 @@ title: Channels
3
3
  description: Per-store distribution surfaces — online storefront, POS, marketplace, wholesale — each with its own product catalog and order attribution.
4
4
  ---
5
5
 
6
- import { Since } from '/snippets/since.mdx';
7
-
8
-
9
6
  ## Overview
10
7
 
11
8
  Channels segment a single [Store](stores.md) into distinct selling surfaces. A channel represents *where* an order originates from — the online storefront, an in-person point-of-sale till, a marketplace integration (Amazon, eBay), a B2B wholesale portal, a mobile app — and *which* subset of the store's products is available there.
@@ -93,7 +90,6 @@ This attribution drives reporting (best-selling by channel, revenue per channel)
93
90
 
94
91
  ### Storefront Access Gating
95
92
 
96
-
97
93
  A channel's `storefront_access` decides what an **anonymous** visitor — a request with no authenticated customer — may see. Logged-in customers are never gated. The posture is one of three values:
98
94
 
99
95
  | Mode | Guest sees catalog | Guest sees prices | Use case |
@@ -54,7 +54,7 @@ Memberships stay always-active and always customer-backed: the pending state liv
54
54
 
55
55
  Carts and orders carry a `company_id` pointing at any node — buying *for* a division means pointing at it. A buyer with exactly one membership resolves to it automatically; a buyer with several names the node on their cart (the `company_id` param on cart update, validated against their standing). The node is frozen onto the order at completion, like every other order attribute, so a placed order's tax treatment stays explainable no matter how the tree changes later.
56
56
 
57
- Exemption certificates and the company's tax registration always resolve through `company.legal_entity` — see [Taxes, Discounts & Fees](/docs/developer/core-concepts/taxes-discounts-fees) for how they reach the tax provider.
57
+ Exemption certificates and the company's tax registration always resolve through `company.legal_entity` — see [Taxes](taxes.md) for how they reach the tax provider.
58
58
 
59
59
  ## Catalogs
60
60
 
@@ -3,8 +3,6 @@ title: Customers
3
3
  description: How Spree models customer accounts — registration, authentication, addresses, order history, store credits, and guest checkout behavior.
4
4
  ---
5
5
 
6
- import { Since } from '/snippets/since.mdx';
7
-
8
6
  ## Overview
9
7
 
10
8
  Customers interact with your store through the Store API. They can register, log in, manage their profile, and view order history.
@@ -193,7 +191,6 @@ On success, the user is automatically logged in and a JWT token is returned. The
193
191
 
194
192
  ## Newsletter Subscriptions
195
193
 
196
-
197
194
  Headless storefronts often need to collect newsletter signups before account creation (footer forms, popup overlays). The Store API exposes a double opt-in subscription flow that mirrors the password reset webhook pattern.
198
195
 
199
196
  ### Step 1: Subscribe
@@ -0,0 +1,133 @@
1
+ ---
2
+ title: Discounts
3
+ description: The record of money taken off one order — where a discount comes from, and why it is spread across the items it applied to.
4
+ ---
5
+
6
+ ## Overview
7
+
8
+ A discount is one line on one order saying money came off, and why.
9
+
10
+ > **NOTE:** **A discount is not a promotion.** A [promotion](promotions.md) is a *rule* — "20% off shoes in July" — that exists on its own, before anybody buys anything. A discount is the *result* on a particular order.
11
+ >
12
+ > Think of the promotion as the sign in the shop window and the discount as the line on the receipt. One promotion produces thousands of discounts over its life.
13
+
14
+ That separation is what lets an order stay truthful. The promotion can be edited, paused or deleted next month; the discount row on last month's order still says exactly what that customer was given.
15
+
16
+ It also means **not every discount has a promotion behind it.** When a support agent takes $10 off as a goodwill gesture, that's a discount with no campaign attached — which is what the `kind` field distinguishes.
17
+
18
+ ```mermaid
19
+ erDiagram
20
+ Promotion ||--o{ Discount : "produces many, over time"
21
+ Order ||--o{ Discount : "has many"
22
+ LineItem ||--o{ Discount : "reduced by"
23
+ Fulfillment ||--o{ Discount : "reduced by"
24
+
25
+ Promotion {
26
+ string name
27
+ string code
28
+ datetime expires_at
29
+ }
30
+ Discount {
31
+ string label
32
+ string kind
33
+ string code
34
+ string value
35
+ string amount
36
+ }
37
+ ```
38
+
39
+ | | [Promotion](promotions.md) | Discount |
40
+ |---|---|---|
41
+ | What it is | A campaign rule | A row on one order |
42
+ | Exists | Before any order | Only once money comes off |
43
+ | How many | One | One per order it applies to |
44
+ | Can change later | Yes — edit or end it | No — it's a financial record |
45
+ | Set up by | A merchant, in advance | Created automatically, or by staff |
46
+
47
+ ## Discount attributes
48
+
49
+ | Attribute | Description |
50
+ |---|---|
51
+ | `label` | What the customer sees — `Summer Sale` |
52
+ | `kind` | `promotion` or `manual` |
53
+ | `code` | The coupon code used, if there was one |
54
+ | `value` / `value_type` | The rule behind it — `"10"` and `percent` |
55
+ | `amount` / `display_amount` | What actually came off, as a negative amount |
56
+ | `promotion_id` | The promotion responsible, for promotion discounts |
57
+ | `line_item_id` / `fulfillment_id` | What it reduced |
58
+
59
+ Note the difference between `value` and `amount`. `value` is the rule — "10 percent". `amount` is the money — "-$12.00". You need the rule to explain the discount and the amount to add up the order.
60
+
61
+ ## Reading discounts on an order
62
+
63
+ Whatever produced them, the rows read the same way:
64
+
65
+ ```typescript Store SDK
66
+ const cart = await client.carts.get(cartId)
67
+
68
+ cart.display_discount_total // "-$12.00"
69
+
70
+ cart.discounts.forEach((discount) => {
71
+ discount.label // "Summer Sale"
72
+ discount.kind // "promotion" or "manual"
73
+ discount.code // "SUMMER20", when a code was used
74
+ discount.display_amount // "-$12.00"
75
+ })
76
+ ```
77
+
78
+ Applying a coupon code, and what decides whether it's accepted, belongs to [Promotions](promotions.md).
79
+
80
+ ## Manual discounts
81
+
82
+ Sometimes there's no promotion — a price match, an apology for a late delivery. Staff can add a discount directly:
83
+
84
+
85
+ ```typescript Admin SDK
86
+ // 10% off one line item
87
+ await adminClient.orders.discounts.create('or_xxx', {
88
+ label: 'Goodwill discount',
89
+ value: '10',
90
+ value_type: 'percent',
91
+ line_item_id: 'li_xxx',
92
+ })
93
+
94
+ // A flat amount across the order
95
+ await adminClient.orders.discounts.create('or_xxx', {
96
+ label: 'Price match',
97
+ value: '15.00',
98
+ value_type: 'flat',
99
+ })
100
+ ```
101
+
102
+ ```bash cURL
103
+ curl -X POST 'https://api.mystore.com/api/v3/admin/orders/or_xxx/discounts' \
104
+ -H 'X-Spree-API-Key: sk_xxx' \
105
+ -H 'Content-Type: application/json' \
106
+ -d '{ "label": "Goodwill discount", "value": "10", "value_type": "percent" }'
107
+ ```
108
+
109
+
110
+ > **WARNING:** Only **manual** discounts can be edited or deleted. Changing one that came from a promotion returns a `422`.
111
+ >
112
+ > That row belongs to the promotion that created it. If staff could edit it, an order would start disagreeing with the promotion it claims to have used — and the reporting behind "how did the summer sale do" stops adding up.
113
+
114
+ ## How a discount is spread
115
+
116
+ An order-level discount isn't kept as one lump sum. It's divided across the items it applied to, in proportion to what they cost.
117
+
118
+ This looks like an implementation detail until someone returns one item out of three. Then the question is: how much of that $30 discount belonged to the returned shirt? If the discount were one lump, you'd have to guess. Because it was spread at the time, the answer is already recorded, and the refund is right without anyone doing arithmetic on the phone.
119
+
120
+ The same reasoning applies to tax — the tax on a partially returned order follows the same split.
121
+
122
+ ## Discounts on placed orders
123
+
124
+ Once an order exists, its discounts stop being regenerated. Editing a placed order re-applies the rows it already has rather than re-running today's promotions.
125
+
126
+ The alternative would mean an order quietly changing because a sale ended overnight — which is indefensible to a customer holding a confirmation email.
127
+
128
+ ## Related
129
+
130
+ - [Promotions](promotions.md) — the rules that create most discounts
131
+ - [Order totals](order-totals.md) — how discounts roll into the total
132
+ - [Gift cards & store credit](store-credits-gift-cards.md) — payment, not discount
133
+ - [Returns](returns-exchanges-claims.md) — how a discount is unwound on a return