@spree/docs 0.1.182 → 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 (29) hide show
  1. package/dist/developer/core-concepts/addresses.md +106 -198
  2. package/dist/developer/core-concepts/architecture.md +97 -126
  3. package/dist/developer/core-concepts/calculators.md +75 -252
  4. package/dist/developer/core-concepts/carts.md +1 -1
  5. package/dist/developer/core-concepts/channels.md +0 -4
  6. package/dist/developer/core-concepts/companies-and-catalogs.md +1 -1
  7. package/dist/developer/core-concepts/customers.md +0 -3
  8. package/dist/developer/core-concepts/discounts.md +133 -0
  9. package/dist/developer/core-concepts/events.md +83 -576
  10. package/dist/developer/core-concepts/fees.md +144 -0
  11. package/dist/developer/core-concepts/imports-exports.md +105 -679
  12. package/dist/developer/core-concepts/inventory.md +114 -248
  13. package/dist/developer/core-concepts/markets.md +9 -12
  14. package/dist/developer/core-concepts/media.md +9 -11
  15. package/dist/developer/core-concepts/metafields.md +123 -200
  16. package/dist/developer/core-concepts/order-totals.md +110 -0
  17. package/dist/developer/core-concepts/orders.md +1 -1
  18. package/dist/developer/core-concepts/payments.md +11 -14
  19. package/dist/developer/core-concepts/pricing.md +11 -13
  20. package/dist/developer/core-concepts/products.md +35 -19
  21. package/dist/developer/core-concepts/promotions.md +12 -11
  22. package/dist/developer/core-concepts/search-filtering.md +2 -4
  23. package/dist/developer/core-concepts/store-credits-gift-cards.md +0 -3
  24. package/dist/developer/core-concepts/taxes.md +125 -113
  25. package/dist/developer/core-concepts/translations.md +61 -68
  26. package/dist/developer/core-concepts/webhooks.md +25 -59
  27. package/dist/developer/how-to/custom-promotion.md +3 -3
  28. package/package.json +1 -1
  29. package/dist/developer/core-concepts/taxes-discounts-fees.md +0 -199
@@ -1,134 +1,127 @@
1
1
  ---
2
2
  title: Translations
3
- description: Translate Spree products, taxons, and other store content into multiple languages using Mobility-backed translations and locale-aware APIs.
3
+ description: Selling in more than one language translating product content, and translating the interface around it.
4
4
  ---
5
5
 
6
6
  ## Overview
7
7
 
8
- Spree supports two types of translations:
8
+ Two different things need translating, and they're handled separately because they belong to different people.
9
9
 
10
- 1. **Resource Translations** — translatable content fields on models like Products, Taxons, and Stores (e.g., product name, description, slug)
11
- 2. **UI Translations** — interface strings used in the admin panel (e.g., button labels, flash messages)
10
+ **Your content** — product names, descriptions, category names is written by whoever runs the store. Spree stores a version of each field per language.
12
11
 
13
- For configuring which locales and currencies are available in your store, see [Markets](markets.md). Markets control locale and currency assignment per geographic region.
12
+ **The interface** button labels, error messages, email wording is written by Spree, and is already translated into dozens of languages by the community.
14
13
 
15
- ## Resource Translations
14
+ > **INFO:** Which languages a region gets is decided by its [Market](markets.md). A market sets the default locale alongside the currency, so a visitor from France lands on French prices and French copy without choosing anything.
16
15
 
17
- Resources with user-facing content fields have built-in support for translations. Each translatable resource has a corresponding translations table in the database — for example, product translations are stored in `spree_product_translations`.
16
+ ## Translating content
17
+
18
+ Every translatable record keeps one row per language, so a product genuinely has a French name rather than a French name pasted over the English one.
18
19
 
19
20
  ```mermaid
20
21
  erDiagram
21
- Product ||--o{ ProductTranslation : "has many"
22
- Taxon ||--o{ TaxonTranslation : "has many"
23
- Store ||--o{ StoreTranslation : "has many"
22
+ Product ||--o{ ProductTranslation : "one per locale"
24
23
 
25
24
  Product {
26
- string id PK
27
25
  string status
26
+ string sku
28
27
  }
29
-
30
28
  ProductTranslation {
31
- string id PK
32
- string product_id FK
33
29
  string locale
34
30
  string name
35
- text description
31
+ string description
36
32
  string slug
37
33
  string meta_title
38
- string meta_description
39
- }
40
-
41
- TaxonTranslation {
42
- string id PK
43
- string taxon_id FK
44
- string locale
45
- string name
46
- text description
47
- string permalink
48
34
  }
49
35
  ```
50
36
 
51
- ### Translatable Fields
37
+ ### What can be translated
52
38
 
53
- | Resource | Translatable Fields |
54
- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
55
- | Product | `name`, `description`, `slug`, `meta_description`, `meta_title` |
56
- | Taxon | `name`, `description`, `permalink` |
57
- | Taxonomy | `name` |
58
- | Option Type | `presentation` |
59
- | Option Value | `presentation` |
60
- | Property | `presentation` |
61
- | Product Property | `value` |
62
- | Store | `name`, `meta_description`, `meta_keywords`, `seo_title`, `customer_support_email`, `address`, `contact_phone` |
39
+ | Resource | Fields |
40
+ |---|---|
41
+ | Product | `name`, `description`, `slug`, `meta_title`, `meta_description` |
42
+ | Category | `name`, `description`, `permalink` |
43
+ | Collection | `name`, `description`, `permalink` |
44
+ | Product type | `name` |
45
+ | Option type | `presentation` |
46
+ | Option value | `presentation` |
47
+ | Store | `name`, `seo_title`, `meta_description`, `meta_keywords`, and support contact details |
48
+ | Policy | `name`, `body` |
49
+ | Seller | `name`, `about` |
63
50
 
64
- ### Store API
51
+ Note that **slugs are translated too**. A French page can live at `/produits/sac-spree` rather than at the English slug with French words on it — which is what search engines actually reward. See [Slugs](slugs.md#internationalization).
65
52
 
66
- To retrieve translated content, pass the locale via the [`X-Spree-Locale` header](../../api-reference/store-api/localization.md) or the SDK `locale` option:
53
+ ## Reading translated content
54
+
55
+ Ask for a locale and you get that language back. Nothing else about the request changes:
67
56
 
68
57
 
69
58
  ```typescript Store SDK
70
- // Fetch product in French
71
- const product = await client.products.get('spree-tote', {}, {
72
- locale: 'fr',
73
- })
59
+ const product = await client.products.get('spree-tote', {}, { locale: 'fr' })
74
60
 
75
61
  product.name // "Sac Spree"
76
- product.description // "Un sac fourre-tout élégant..."
62
+ product.description // "Un sac fourre-tout élégant"
77
63
  product.slug // "sac-spree"
78
64
 
79
- // List categories in German
80
- const { data: categories } = await client.categories.list({}, {
81
- locale: 'de',
82
- })
65
+ const { data: categories } = await client.categories.list({}, { locale: 'de' })
83
66
  ```
84
67
 
85
68
  ```typescript Admin SDK
86
- // Pass the locale as a request option to get translated content.
87
- // The Admin API resolves by prefixed ID only — slugs are not accepted.
69
+ // The Admin API resolves by ID only slugs are not accepted here
88
70
  const product = await adminClient.products.get('prod_86Rf07xd4z', {}, { locale: 'fr' })
89
-
90
- const { data: categories } = await adminClient.categories.list({}, { locale: 'de' })
91
71
  ```
92
72
 
93
73
  ```bash cURL
94
- # Fetch product in French
95
74
  curl 'https://api.mystore.com/api/v3/store/products/spree-tote' \
96
75
  -H 'X-Spree-API-Key: pk_xxx' \
97
76
  -H 'X-Spree-Locale: fr'
98
77
  ```
99
78
 
100
79
 
101
- Slugs are also localized a product can have different slugs per locale. See [Slugs](slugs.md#internationalization) for details.
80
+ If a field has no translation for the requested locale, you get the default-language value rather than an empty string — a half-translated catalogue still renders as a usable page.
81
+
82
+ > **NOTE:** Responses vary by locale, so if you cache them, include the locale in the cache key. Spree sets `Vary` headers for you, which is enough for a CDN but not for a hand-rolled in-memory cache.
83
+
84
+ ## Writing translations
102
85
 
103
- ### Managing Translations
86
+ Merchants translate in the dashboard, switching locale on the record they're editing. For bulk work — handing a catalogue to a translation agency — export to CSV, translate, and import it back.
104
87
 
105
- Translations are managed in the Admin Panel. When editing a product, taxon, or other translatable resource, switch the locale selector to enter content in each language.
88
+ Translations can also be written through the Admin API, which is what you'd use to sync from an external translation service:
106
89
 
107
- Translations can also be [managed via the Admin API](../sdk/admin/quickstart.md).
90
+ ```typescript Admin SDK
91
+ await adminClient.products.update('prod_xxx', {
92
+ translations: {
93
+ fr: { name: 'Sac Spree', description: 'Un sac fourre-tout élégant…' },
94
+ de: { name: 'Spree Tasche' },
95
+ },
96
+ })
97
+
98
+ // Which resources and fields accept translations
99
+ const { data: resources } = await adminClient.translatableResources.list()
100
+ ```
108
101
 
109
- ## UI Translations
102
+ Asking the API which resources are translatable means a translation tool doesn't need a hardcoded list that goes stale.
110
103
 
111
- Spree stores UI translation strings in a separate project: [Spree I18n](https://github.com/spree-contrib/spree_i18n). This is a community-maintained project with locale files for 43+ languages.
104
+ ## Translating the interface
112
105
 
113
- To install UI translations:
106
+ Spree's own strings — everything in the dashboard and in transactional emails — come from a community-maintained project covering 40+ languages.
114
107
 
115
108
 
116
- ```bash Spree CLI (Docker)
109
+ ```bash Spree CLI
117
110
  spree bundle add spree_i18n
118
111
  ```
119
112
 
120
- ```bash Without Spree CLI
113
+ ```bash Bundler
121
114
  bundle add spree_i18n
122
115
  ```
123
116
 
124
117
 
125
- Once installed, all translation files are available automatically no need to copy any files.
118
+ That's the whole installation. Locales are picked up automatically; nothing needs copying into your app.
126
119
 
127
- > **INFO:** The full list of supported locales is available in the [Spree I18n GitHub repository](https://github.com/spree-contrib/spree_i18n/tree/main/config/locales).
120
+ > **INFO:** See the [supported locales](https://github.com/spree-contrib/spree_i18n/tree/main/config/locales) in the Spree I18n repository. Contributions are welcome if yours is incomplete.
128
121
 
129
- ## Related Documentation
122
+ ## Related
130
123
 
131
- - [Markets](markets.md) — Locale and currency configuration per geographic region
132
- - [Localization](../../api-reference/store-api/localization.md) — Locale, currency, and country headers in API requests
133
- - [Slugs](slugs.md) — Localized slugs and URL identifiers
134
- - [Products](products.md) — Product translations
124
+ - [Markets](markets.md) — which locale and currency a region gets
125
+ - [Slugs](slugs.md) — localized URLs
126
+ - [Custom Fields](metafields.md) — translating your own fields
127
+ - [Localization](../../api-reference/store-api/localization.md) — the locale, currency and country headers
@@ -3,9 +3,6 @@ title: Webhooks
3
3
  description: Send real-time HTTP notifications to external services when events occur in your store.
4
4
  ---
5
5
 
6
- import { Since } from '/snippets/since.mdx';
7
-
8
-
9
6
  ## Overview
10
7
 
11
8
  Webhooks allow your Spree store to send real-time HTTP POST notifications to external services when events occur. When an order is completed, a product is updated, or inventory changes, Spree can automatically notify your CRM, fulfillment service, analytics platform, or any other system.
@@ -44,7 +41,7 @@ flowchart LR
44
41
  end
45
42
  ```
46
43
 
47
- 1. An event is published (e.g., `order.completed`)
44
+ 1. An event is published (e.g., `order.placed`)
48
45
  2. The `WebhookEventSubscriber` receives all events
49
46
  3. It finds active webhook endpoints subscribed to that event
50
47
  4. For each endpoint, it creates a `WebhookDelivery` record and queues a job
@@ -107,13 +104,13 @@ The `subscriptions` attribute controls which events trigger webhooks to this end
107
104
 
108
105
  ```typescript Admin SDK
109
106
  await client.webhookEndpoints.update('whe_xxx', {
110
- subscriptions: ['order.completed', 'order.canceled'],
107
+ subscriptions: ['order.placed', 'order.canceled'],
111
108
  })
112
109
  ```
113
110
 
114
111
  ```bash CLI
115
112
  spree api patch /webhook_endpoints/whe_xxx \
116
- -d '{"subscriptions": ["order.completed", "order.canceled"]}'
113
+ -d '{"subscriptions": ["order.placed", "order.canceled"]}'
117
114
  ```
118
115
 
119
116
 
@@ -121,7 +118,7 @@ The `subscriptions` array accepts exact event names and wildcard patterns:
121
118
 
122
119
  | `subscriptions` value | Receives |
123
120
  |---|---|
124
- | `['order.completed', 'order.canceled']` | Only those two events |
121
+ | `['order.placed', 'order.canceled']` | Only those two events |
125
122
  | `['order.*']` | All order events |
126
123
  | `['*.created']` | All creation events |
127
124
  | `['order.*', 'payment.*', 'shipment.shipped']` | Multiple patterns |
@@ -134,7 +131,7 @@ Each webhook delivery sends a JSON payload with the following structure. The `da
134
131
  ```json
135
132
  {
136
133
  "id": "550e8400-e29b-41d4-a716-446655440000",
137
- "name": "order.completed",
134
+ "name": "order.placed",
138
135
  "created_at": "2025-01-15T10:30:00Z",
139
136
  "data": {
140
137
  "id": "or_m3Rp9wXz",
@@ -158,7 +155,7 @@ Each webhook delivery sends a JSON payload with the following structure. The `da
158
155
  | Field | Description |
159
156
  |-------|-------------|
160
157
  | `id` | Unique UUID for this event |
161
- | `name` | Event name (e.g., `order.completed`) |
158
+ | `name` | Event name (e.g., `order.placed`) |
162
159
  | `created_at` | ISO8601 timestamp when event occurred |
163
160
  | `data` | Serialized resource data (V3 API format with [prefixed IDs](../../api-reference/introduction.md)) |
164
161
  | `metadata` | Additional context including Spree version |
@@ -175,7 +172,7 @@ Each webhook request includes these headers:
175
172
  |--------|-------------|
176
173
  | `Content-Type` | `application/json` |
177
174
  | `User-Agent` | `Spree-Webhooks/1.0` |
178
- | `X-Spree-Webhook-Event` | Event name (e.g., `order.completed`) |
175
+ | `X-Spree-Webhook-Event` | Event name (e.g., `order.placed`) |
179
176
  | `X-Spree-Webhook-Signature` | HMAC-SHA256 signature for verification |
180
177
  | `X-Spree-Webhook-Timestamp` | Unix timestamp (seconds) generated at send time; included in the HMAC-SHA256 signed string as `{timestamp}.{payload}` and required to verify `X-Spree-Webhook-Signature` |
181
178
 
@@ -227,7 +224,7 @@ class WebhooksController < ApplicationController
227
224
  event = JSON.parse(request.body.read)
228
225
 
229
226
  case event['name']
230
- when 'order.completed'
227
+ when 'order.placed'
231
228
  handle_order_completed(event['data'])
232
229
  when 'product.updated'
233
230
  handle_product_updated(event['data'])
@@ -271,7 +268,7 @@ await client.webhookEndpoints.deliveries.redeliver('whe_xxx', 'whd_xxx')
271
268
  ```
272
269
 
273
270
  ```bash CLI
274
- spree api get /webhook_endpoints/whe_xxx/deliveries -q event_name_eq=order.completed
271
+ spree api get /webhook_endpoints/whe_xxx/deliveries -q event_name_eq=order.placed
275
272
  spree api post /webhook_endpoints/whe_xxx/deliveries/whd_xxx/redeliver
276
273
  ```
277
274
 
@@ -292,24 +289,9 @@ spree api post /webhook_endpoints/whe_xxx/deliveries/whd_xxx/redeliver
292
289
 
293
290
  ## Configuration
294
291
 
295
- ### Enabling/Disabling Webhooks
296
-
297
- Webhooks are enabled by default. To disable globally:
292
+ Webhooks are on by default and can be switched off installation-wide.
298
293
 
299
- ```ruby
300
- # config/initializers/spree.rb
301
- Spree::Api::Config.webhooks_enabled = false
302
- ```
303
-
304
- ### SSL Verification
305
-
306
- SSL verification is enabled by default in production. In development, it's disabled to allow testing with self-signed certificates:
307
-
308
- ```ruby
309
- # config/initializers/spree.rb
310
- Spree::Api::Config.webhooks_verify_ssl = true # Force SSL verification
311
- Spree::Api::Config.webhooks_verify_ssl = false # Disable (not recommended for production)
312
- ```
294
+ SSL certificates are verified in production, and not in development — so you can point an endpoint at a local tunnel with a self-signed certificate while building. Both are configurable if your environment needs something different.
313
295
 
314
296
  ## Available Events
315
297
 
@@ -319,14 +301,16 @@ Common webhook events include:
319
301
 
320
302
  | Event | Description |
321
303
  |-------|-------------|
322
- | `order.completed` | Order checkout finished |
323
- | `order.canceled` | Order was canceled |
324
- | `order.paid` | Order is fully paid |
325
- | `shipment.shipped` | Shipment was shipped |
326
- | `payment.paid` | Payment was completed |
327
- | `product.created` | New product created |
328
- | `product.updated` | Product was modified |
329
- | `user.created` | New customer/user registered (the admin user class emits `admin.created`) |
304
+ | `order.placed` | A customer completed checkout |
305
+ | `order.paid` | The order is fully paid |
306
+ | `order.canceled` | The order was canceled |
307
+ | `order.shipped` | Everything on the order has shipped |
308
+ | `fulfillment.shipped` | One parcel went out |
309
+ | `payment.completed` | A payment succeeded |
310
+ | `product.created` / `product.updated` | Catalog changes |
311
+ | `product.out_of_stock` / `product.back_in_stock` | Availability flipped |
312
+ | `customer.created` | A new customer registered |
313
+ | `return.received` | A return arrived |
330
314
 
331
315
  ## Testing Webhooks
332
316
 
@@ -358,24 +342,6 @@ spree api post /webhook_endpoints/whe_xxx/send_test
358
342
 
359
343
  `send_test` delivers a synthetic `webhook.test` event so you can verify the endpoint is reachable and your signature-verification code works, without having to trigger a real order.
360
344
 
361
- ### In Tests
362
-
363
- ```ruby
364
- RSpec.describe 'Webhook delivery' do
365
- let(:store) { create(:store) }
366
- let(:endpoint) { create(:webhook_endpoint, store: store, subscriptions: ['order.completed']) }
367
- let(:order) { create(:completed_order_with_totals, store: store) }
368
-
369
- it 'delivers webhook when order completes' do
370
- stub_request(:post, endpoint.url).to_return(status: 200)
371
-
372
- expect {
373
- order.publish_event('order.completed')
374
- }.to have_enqueued_job(Spree::WebhookDeliveryJob)
375
- end
376
- end
377
- ```
378
-
379
345
  ## Best Practices
380
346
 
381
347
 
@@ -392,10 +358,10 @@ end
392
358
 
393
359
  ### Webhooks Not Delivering
394
360
 
395
- 1. Check that webhooks are enabled: `Spree::Api::Config.webhooks_enabled`
396
- 2. Verify the endpoint is active: `endpoint.active?`
397
- 3. Confirm the endpoint subscribes to the event: `endpoint.subscribed_to?('order.completed')`
398
- 4. Check the event has a `store_id` matching the endpoint's store
361
+ 1. Check that webhooks are enabled installation-wide
362
+ 2. Verify the endpoint is marked active
363
+ 3. Confirm the endpoint actually subscribes to the event you expect
364
+ 4. Check the event belongs to the same store as the endpoint
399
365
 
400
366
  ### Signature Verification Failing
401
367
 
@@ -8,7 +8,7 @@ description: Step-by-step guide to creating custom promotion rules, discount act
8
8
  Spree's promotion system has three extension points:
9
9
 
10
10
  - **Rules** — conditions that decide when a promotion applies
11
- - **Actions** — what an applied promotion does, usually writing [Discount](../core-concepts/taxes-discounts-fees.md#discounts) rows
11
+ - **Actions** — what an applied promotion does, usually writing [Discount](../core-concepts/discounts.md) rows
12
12
  - **Adjusters** — for discounts and fees that aren't promotions at all (loyalty pricing, gift wrap fees, payment surcharges)
13
13
 
14
14
  Spree ships with a comprehensive set of [built-in rules and actions](../core-concepts/promotions.md#rules). This guide shows how to build your own of each kind.
@@ -238,7 +238,7 @@ Like rules, registered actions surface automatically in the dashboard's promotio
238
238
 
239
239
  ## Custom Adjusters
240
240
 
241
- Not every charge or reduction is a promotion. A gift wrap fee, a payment surcharge, or loyalty pricing has no rules, no coupon codes, and no competition — it just needs to be on the order whenever it applies. That's an **adjuster**: a class invoked on every recalculation that owns a family of [Fee or Discount](../core-concepts/taxes-discounts-fees.md) rows.
241
+ Not every charge or reduction is a promotion. A gift wrap fee, a payment surcharge, or loyalty pricing has no rules, no coupon codes, and no competition — it just needs to be on the order whenever it applies. That's an **adjuster**: a class invoked on every recalculation that owns a family of [Fee](../core-concepts/fees.md) or [Discount](../core-concepts/discounts.md) rows.
242
242
 
243
243
  ```ruby app/models/my_app/adjusters/gift_wrap.rb
244
244
  module MyApp
@@ -329,6 +329,6 @@ end
329
329
  ## Related Documentation
330
330
 
331
331
  - [Promotions](../core-concepts/promotions.md) — promotion architecture and built-in rules and actions
332
- - [Taxes, Discounts & Fees](../core-concepts/taxes-discounts-fees.md) — the typed rows actions and adjusters write
332
+ - [Discounts](../core-concepts/discounts.md) and [Fees](../core-concepts/fees.md) — the rows actions and adjusters write
333
333
  - [Calculators](../core-concepts/calculators.md) — available calculator types for promotion actions
334
334
  - [Events](../core-concepts/events.md) — subscribe to promotion events
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spree/docs",
3
- "version": "0.1.182",
3
+ "version": "0.1.183",
4
4
  "description": "Spree Commerce developer documentation for AI agents and local reference",
5
5
  "type": "module",
6
6
  "license": "CC-BY-4.0",
@@ -1,199 +0,0 @@
1
- ---
2
- title: Taxes, Discounts & Fees
3
- description: How Spree records money added to or taken off an order — separate tax lines, discounts and fees, and the order totals you render in a storefront.
4
- ---
5
-
6
- ## Overview
7
-
8
- An order total is rarely just the sum of item prices. Tax gets added, a promo code takes something off, gift wrapping costs extra. Spree records each of those as its own row, of one of three kinds:
9
-
10
- | Kind | Effect | What it is |
11
- |---|---|---|
12
- | **Tax line** | adds | Tax charged on an item, on delivery, or on a fee |
13
- | **Discount** | subtracts | A promotion or a manual reduction |
14
- | **Fee** | adds | A surcharge — handling, gift wrap, cash on delivery |
15
-
16
- Keeping them apart means a direct question gets a direct answer. "What tax did we charge on this order?" is one request against tax lines, rather than filtering a mixed list and hoping you caught every case.
17
-
18
- ```mermaid
19
- erDiagram
20
- Order ||--o{ TaxLine : "has many"
21
- Order ||--o{ Discount : "has many"
22
- Order ||--o{ Fee : "has many"
23
- LineItem ||--o{ TaxLine : "taxed by"
24
- LineItem ||--o{ Discount : "reduced by"
25
- Fulfillment ||--o{ TaxLine : "taxed by"
26
- Fee ||--o{ TaxLine : "taxed by"
27
-
28
- TaxLine {
29
- string label
30
- string rate
31
- boolean included
32
- string amount
33
- }
34
- Discount {
35
- string label
36
- string kind
37
- string code
38
- string amount
39
- }
40
- Fee {
41
- string label
42
- string kind
43
- string amount
44
- }
45
- ```
46
-
47
- > **NOTE:** Money is sent as a **string** — `"15.99"`, not `15.99`. JavaScript numbers can't represent every decimal exactly, and `0.1 + 0.2` gives `0.30000000000000004`, which is not something you want inside a total. If you need to do arithmetic, use a decimal library or work in whole cents.
48
-
49
- ## Building an order summary
50
-
51
- Most storefronts never need the individual rows. The order already carries a total for each kind, so you can render a summary directly:
52
-
53
- ```typescript
54
- const order = await client.orders.get('or_xxx')
55
-
56
- order.display_item_total // "$120.00" items before tax and discounts
57
- order.display_discount_total // "-$12.00" everything taken off
58
- order.display_delivery_total // "$5.00" delivery
59
- order.display_tax_total // "$22.60" all tax
60
- order.display_total // "$135.60" what the customer pays
61
- ```
62
-
63
- Every total comes in two forms: `total` is the raw value (`"135.60"`) and `display_total` is formatted for the order's currency (`"$135.60"`). Render the `display_` one — it handles the symbol, decimal separator and where the symbol goes.
64
-
65
- | Attribute | What it sums |
66
- |---|---|
67
- | `item_total` | Line item prices before tax and discounts |
68
- | `discount_total` | All discounts |
69
- | `tax_total` | All tax |
70
- | `included_tax_total` | Tax already inside displayed prices |
71
- | `additional_tax_total` | Tax added on top |
72
- | `delivery_total` | Delivery charges |
73
- | `total` | What the customer pays |
74
-
75
- > **WARNING:** If you add up a subtotal yourself, include `additional_tax_total` only. `included_tax_total` is **already inside** the item prices — adding it again counts the tax twice. This is the most common mistake when building a European storefront.
76
-
77
- ## Tax lines
78
-
79
- A tax line records one tax charge and what it was charged on.
80
-
81
- | Attribute | Description |
82
- |---|---|
83
- | `label` | What the customer sees, e.g. `VAT 20%` |
84
- | `rate` | The rate applied, e.g. `"0.2"` |
85
- | `included` | Whether the tax was already inside the displayed price |
86
- | `amount` / `display_amount` | Tax charged |
87
- | `line_item_id` / `fulfillment_id` / `fee_id` | What was taxed — exactly one is set |
88
- | `tax_rate_id` | The configured rate, when tax came from Spree rather than an outside service |
89
-
90
- `included` is the field that trips people up. In most of Europe a €120 price already contains €20 of VAT, so the tax line is there for information — the customer pays €120, not €140. In the US, tax is added at checkout, so the same field is `false` and the amount really is extra.
91
-
92
- ```typescript Admin SDK
93
- const taxLines = await adminClient.orders.taxLines.list('or_xxx')
94
- ```
95
-
96
- You never create tax lines yourself. They're written whenever the order total is worked out — see [Taxes](taxes.md).
97
-
98
- ## Discounts
99
-
100
- | Attribute | Description |
101
- |---|---|
102
- | `label` | What the customer sees, e.g. `Summer Sale` |
103
- | `kind` | `promotion` or `manual` |
104
- | `code` | The coupon code used, if any |
105
- | `value` / `value_type` | The rule behind it, e.g. `"10"` / `percent`. Accepts a string or a number on write. |
106
- | `amount` / `display_amount` | The reduction, as a negative amount |
107
- | `promotion_id` | The promotion it came from |
108
- | `line_item_id` / `fulfillment_id` | What it reduced |
109
-
110
- Most discounts come from [Promotions](promotions.md) — a customer applies a code and the rows appear. A **manual** discount is one an admin adds directly, for a goodwill gesture or a price match:
111
-
112
-
113
- ```typescript Admin SDK
114
- // 10% off a single line item
115
- await adminClient.orders.discounts.create('or_xxx', {
116
- label: 'Goodwill discount',
117
- value: '10',
118
- value_type: 'percent',
119
- line_item_id: 'li_xxx',
120
- })
121
-
122
- // A flat amount across the order
123
- await adminClient.orders.discounts.create('or_xxx', {
124
- label: 'Price match',
125
- value: '15.00',
126
- value_type: 'flat',
127
- })
128
- ```
129
-
130
- ```bash cURL
131
- curl -X POST 'https://api.mystore.com/api/v3/admin/orders/or_xxx/discounts' \
132
- -H 'X-Spree-API-Key: sk_xxx' \
133
- -H 'Content-Type: application/json' \
134
- -d '{ "label": "Goodwill discount", "value": 10, "value_type": "percent" }'
135
- ```
136
-
137
-
138
- Only manual discounts can be edited or deleted. Changing one that came from a promotion returns a `422` — those belong to the promotion that created them, and letting an order disagree with its own promotion is how reporting stops adding up.
139
-
140
- A discount on the whole order is shared out across the items rather than kept as one lump sum. So if the customer later returns one item, Spree knows how much of the discount belonged to it.
141
-
142
- ## Fees
143
-
144
- | Attribute | Description |
145
- |---|---|
146
- | `label` | What the customer sees, e.g. `Gift wrapping` |
147
- | `kind` | `surcharge`, `handling`, `gift_wrap`, `cod`, `payment`, `duty`, or your own |
148
- | `amount` / `display_amount` | The charge |
149
- | `line_item_id` / `fulfillment_id` | What it applies to — both empty means the whole order |
150
-
151
- ```typescript Admin SDK
152
- await adminClient.orders.fees.create('or_xxx', {
153
- label: 'Gift wrapping',
154
- kind: 'gift_wrap',
155
- amount: '5.00',
156
- line_item_id: 'li_xxx',
157
- })
158
- ```
159
-
160
- Fees are always positive, and they're taxable by default — a fee can have its own tax lines. Customs duties are the one exception, described below. To take money off, create a discount instead. That way a refund never has to work out whether a negative fee was a charge or a credit.
161
-
162
- ### Customs duties
163
-
164
- A `duty` fee is the one kind tax is **not** applied to automatically. A customs duty is an import charge levied by the destination country, not a sale that domestic tax applies to, so charging tax on top of it would invent tax the merchant never owed. Where import VAT is charged on a duty, whoever calculates the duty records that tax itself.
165
-
166
- A duty also keeps a copy of what it was calculated from — the HS code, the country of origin, the rate — in its `metadata`. Classification on a product can change next month; what the customer was charged must not.
167
-
168
- ## Where rows attach
169
-
170
- Each row points at exactly the thing it applies to:
171
-
172
- - **Tax lines** → a line item, a [fulfillment](fulfillments.md), or a fee, so item tax and delivery tax stay separately visible
173
- - **Discounts** → a line item or a fulfillment
174
- - **Fees** → a line item, a fulfillment, or the order itself
175
-
176
- Rows also keep a copy of where they came from — the rate and label on a tax line, the code and promotion on a discount. If someone deletes that promotion next month, the order still says what the customer was actually given.
177
-
178
- ## When totals change
179
-
180
- Totals are worked out again whenever something changes on a cart — an item added, an address entered, a code applied. The response to every cart change already includes the new totals, so a follow-up request is never needed:
181
-
182
- ```typescript
183
- // The returned cart already has updated totals
184
- const cart = await client.carts.items.create(cartId, {
185
- variant_id: 'var_xxx',
186
- quantity: 2,
187
- })
188
-
189
- cart.display_total // already correct
190
- ```
191
-
192
- Once an order is placed, the rows stop being regenerated. Editing a placed order re-adds its existing rows rather than starting from scratch, so today's promotions can't quietly rewrite what a customer agreed to last week.
193
-
194
- ## Related
195
-
196
- - [Taxes](taxes.md) — how tax is worked out
197
- - [Promotions](promotions.md) — where most discounts come from
198
- - [Fulfillments](fulfillments.md) — delivery charges
199
- - [Orders](orders.md) — order totals and statuses