@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.
- package/dist/developer/core-concepts/addresses.md +106 -198
- package/dist/developer/core-concepts/architecture.md +97 -126
- package/dist/developer/core-concepts/calculators.md +75 -252
- package/dist/developer/core-concepts/carts.md +1 -1
- package/dist/developer/core-concepts/catalogs.md +140 -0
- package/dist/developer/core-concepts/channels.md +0 -4
- package/dist/developer/core-concepts/commissions.md +253 -0
- package/dist/developer/core-concepts/companies.md +240 -0
- package/dist/developer/core-concepts/customers.md +0 -3
- package/dist/developer/core-concepts/discounts.md +133 -0
- package/dist/developer/core-concepts/events.md +83 -576
- package/dist/developer/core-concepts/fees.md +144 -0
- package/dist/developer/core-concepts/imports-exports.md +105 -679
- package/dist/developer/core-concepts/inventory.md +114 -248
- package/dist/developer/core-concepts/markets.md +9 -12
- package/dist/developer/core-concepts/media.md +127 -16
- package/dist/developer/core-concepts/metafields.md +123 -200
- package/dist/developer/core-concepts/order-totals.md +110 -0
- package/dist/developer/core-concepts/orders.md +1 -1
- package/dist/developer/core-concepts/payments.md +11 -14
- package/dist/developer/core-concepts/pricing.md +11 -13
- package/dist/developer/core-concepts/products.md +191 -62
- package/dist/developer/core-concepts/promotions.md +12 -11
- package/dist/developer/core-concepts/search-filtering.md +2 -4
- package/dist/developer/core-concepts/sellers.md +210 -0
- package/dist/developer/core-concepts/staff-roles.md +56 -23
- package/dist/developer/core-concepts/store-credits-gift-cards.md +0 -3
- package/dist/developer/core-concepts/taxes.md +125 -113
- package/dist/developer/core-concepts/translations.md +61 -68
- package/dist/developer/core-concepts/webhooks.md +25 -59
- package/dist/developer/how-to/custom-promotion.md +3 -3
- package/package.json +1 -1
- package/dist/developer/core-concepts/companies-and-catalogs.md +0 -81
- 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
323
|
-
| `order.
|
|
324
|
-
| `order.
|
|
325
|
-
| `
|
|
326
|
-
| `
|
|
327
|
-
| `
|
|
328
|
-
| `product.updated` |
|
|
329
|
-
| `
|
|
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
|
|
396
|
-
2. Verify the endpoint is
|
|
397
|
-
3. Confirm the endpoint subscribes to the event
|
|
398
|
-
4. Check the event
|
|
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/
|
|
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/
|
|
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
|
-
- [
|
|
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,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
|