@spree/docs 0.1.181 → 0.1.183
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-reference/seller-api/errors.md +1 -1
- package/dist/api-reference/store.yaml +78 -91
- 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/channels.md +0 -4
- package/dist/developer/core-concepts/companies-and-catalogs.md +1 -1
- 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 +9 -11
- 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 +173 -19
- package/dist/developer/core-concepts/promotions.md +12 -11
- package/dist/developer/core-concepts/search-filtering.md +2 -4
- 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/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,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
|