@spree/docs 0.1.182 → 0.1.184

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 (34) 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/catalogs.md +140 -0
  6. package/dist/developer/core-concepts/channels.md +0 -4
  7. package/dist/developer/core-concepts/commissions.md +253 -0
  8. package/dist/developer/core-concepts/companies.md +240 -0
  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 +127 -16
  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 +191 -62
  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/sellers.md +210 -0
  26. package/dist/developer/core-concepts/staff-roles.md +56 -23
  27. package/dist/developer/core-concepts/store-credits-gift-cards.md +0 -3
  28. package/dist/developer/core-concepts/taxes.md +125 -113
  29. package/dist/developer/core-concepts/translations.md +61 -68
  30. package/dist/developer/core-concepts/webhooks.md +25 -59
  31. package/dist/developer/how-to/custom-promotion.md +3 -3
  32. package/package.json +1 -1
  33. package/dist/developer/core-concepts/companies-and-catalogs.md +0 -81
  34. package/dist/developer/core-concepts/taxes-discounts-fees.md +0 -199
@@ -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.184",
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,81 +0,0 @@
1
- ---
2
- title: Companies & Catalogs
3
- description: How Spree models B2B buyer organizations — a multi-level company tree with members and an address book — and the catalogs that decide what each audience sees and pays.
4
- ---
5
-
6
- ## Overview
7
-
8
- A business customer in Spree is a **tree of company nodes**, not a flat account. One node can stand alone — a company with a few members and addresses — or grow into a group: subsidiaries, divisions, regional units, each with its own buyers and ship-to sites.
9
-
10
- ```mermaid
11
- erDiagram
12
- Company ||--o{ Company : "parent of"
13
- Company ||--o{ CompanyMembership : "has many"
14
- Company ||--o{ CompanyAddress : "has many"
15
- Company ||--o{ CompanyInvitation : "has many"
16
- Company ||--o{ TaxIdentifier : "legal entities only"
17
- Company ||--o{ TaxExemptionCertificate : "legal entities only"
18
-
19
- Company {
20
- string name
21
- string kind "company | division"
22
- string external_id
23
- }
24
- CompanyMembership {
25
- string role "cosmetic label"
26
- }
27
- CompanyAddress {
28
- string label
29
- boolean default_billing
30
- boolean default_shipping
31
- }
32
- CompanyInvitation {
33
- string email
34
- string token
35
- datetime expires_at
36
- }
37
- ```
38
-
39
- Three ideas carry the whole model:
40
-
41
- - **The tree is structure.** `Spree::Company` is self-referential (`parent_id`), store-scoped, and shallow — depth is capped at five levels. How many ship-to sites a node has never dictates how many nodes exist; addresses are just an address book.
42
- - **Nodes are typed, and the type decides tax.** A `company` node is a legal entity; a `division` is an organizational unit. Tax registrations and exemption certificates exist only on legal entities, and a purchase's tax always resolves through the node's `legal_entity` — for a `company` that is the node itself, and for a `division` it is its nearest `company` ancestor, whose registrations then cover the division's purchases. The walk stops at that first `company` node whether or not it holds any registration, so a subsidiary with no VAT number of its own has none — it never borrows its parent's.
43
- - **Membership is standing over a subtree.** A `CompanyMembership` joins a customer to a node, and that standing covers the node and everything below it. Any authorization question — "may this customer act for node N?" — is a membership check on N and its ancestors, never an equality check on one node.
44
-
45
- ## Members and invitations
46
-
47
- People join by email, from either side — staff in the dashboard or an existing member on the storefront. An email that matches a store customer becomes a membership immediately; an unknown email becomes a `Spree::CompanyInvitation` — a plaintext-token record with a 30-day expiry, whose invite email links to the storefront acceptance page. Accepting either registers a new account (through the standard customer-creation flow, with the invited email) or binds the invitation to the signed-in customer, and lands as a membership either way.
48
-
49
- Memberships stay always-active and always customer-backed: the pending state lives on the invitation, never as a status on the membership.
50
-
51
- **In open source, every member can do everything within their standing** — buy, see the subtree's purchases, manage addresses and members. There are no company roles; the `role` string on a membership is a cosmetic label. Finer control (roles, approvals, spending limits) is an Enterprise layer that enforces through the storefront access policy class (`Spree::Dependencies.storefront_access_policy_class`) and the checkout `validate` hooks — injection points that are deliberate no-ops in OSS.
52
-
53
- ## Purchases
54
-
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
-
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.
58
-
59
- ## Catalogs
60
-
61
- A `Spree::Catalog` narrows what an audience sees and — through an optional price list — what they pay.
62
-
63
- ```mermaid
64
- erDiagram
65
- Catalog ||--o{ CatalogProduct : "assortment"
66
- Catalog ||--o{ CatalogAssignment : "audiences"
67
- Catalog }o--o| PriceList : "optional pricing"
68
- CatalogAssignment }o--|| Company : "or"
69
- CatalogAssignment }o--|| CustomerGroup : "or"
70
- CatalogAssignment }o--|| Market : "or"
71
- CatalogAssignment }o--|| Channel : "or"
72
- ```
73
-
74
- - **Assortment** — a positioned product join, and the switch between a catalog's two modes. A *curated* assortment restricts: the audience sees only what's in it. An *empty* assortment is a pricing-only overlay: the attached price list applies and nothing is hidden — which is how "everyone sees the public store, this company just gets special prices" is expressed. An explicit import action copies the price list's products into the assortment when a restrictive catalog should start from the priced range.
75
- - **Audiences** — assignments to a channel, a customer group, a market, or a company node. A company assignment covers the node's **subtree**: assign the group-wide catalog once at the root, and a branch adds its own extra catalog without re-assigning the shared one.
76
- - **Visibility** — a buyer resolving to a company node sees the union of the effective catalogs on the node and its ancestors; otherwise their customer group's catalogs apply; otherwise every channel listing (narrowed to the channel's default catalog when one is set). Any effective catalog with an empty assortment lifts the restriction — the union then includes the whole range. Gated storefront access runs first — a login-gated guest never reaches this resolution.
77
- - **Pricing** — the effective catalogs' price lists are checked nearest node first (the purchase node's own assignments before its parent's), then the ordinary rule-matched price lists, then base prices. A price list attached to a catalog applies *because the catalog applies* — its own rules are not consulted, and it is excluded from generic rule matching so a rule-less list cannot leak to every shopper.
78
-
79
- ## Storefront self-service
80
-
81
- The Store API ships a full company directory for members: their memberships with ancestor paths (`GET /store/account/companies`), node details and renaming, the address book, members and invitations, and the subtree's completed orders. Authorization on every endpoint is **standing plus the access policy** — never roles, never CanCanCan. The deliberate trade-off: within a company, OSS trusts every member; merchants who need restraint layer the Enterprise governance on the same injection points, with no schema changes.
@@ -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