@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
|
@@ -1,148 +1,125 @@
|
|
|
1
1
|
---
|
|
2
|
-
title:
|
|
3
|
-
description: Add
|
|
2
|
+
title: Custom Fields
|
|
3
|
+
description: Add your own structured, typed data to products, orders and other records — searchable, filterable, and editable in the dashboard.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
import { Since } from '/snippets/since.mdx';
|
|
7
|
-
|
|
8
|
-
|
|
9
6
|
## Overview
|
|
10
7
|
|
|
11
|
-
|
|
8
|
+
Sooner or later you need to store something Spree doesn't have a field for. A fabric composition. A care instruction. A gift message. An ID from the system you sync with.
|
|
9
|
+
|
|
10
|
+
Custom fields are how you do that without changing the database. You declare a field once — its name, its type, who can see it — and from then on it can be set on any record of that kind, edited in the dashboard, returned by the API, and searched or filtered like any built-in field.
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
> **NOTE:** Custom fields were called **metafields** before Spree 6.0. The API and SDK have always called them custom fields; now the rest of Spree does too.
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
- Custom business logic fields
|
|
17
|
-
- Integration data from external systems
|
|
18
|
-
- Order-specific custom attributes
|
|
14
|
+
## Definitions and values
|
|
19
15
|
|
|
20
|
-
|
|
16
|
+
There are two halves, and it helps to keep them straight:
|
|
21
17
|
|
|
22
18
|
```mermaid
|
|
23
19
|
erDiagram
|
|
24
|
-
|
|
25
|
-
Product ||--o{
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
CustomFieldDefinition ||--o{ CustomField : "gives shape to"
|
|
21
|
+
Product ||--o{ CustomField : "has values"
|
|
22
|
+
Order ||--o{ CustomField : "has values"
|
|
23
|
+
Customer ||--o{ CustomField : "has values"
|
|
28
24
|
|
|
29
|
-
|
|
25
|
+
CustomFieldDefinition {
|
|
30
26
|
string namespace
|
|
31
27
|
string key
|
|
32
|
-
string
|
|
33
|
-
string
|
|
28
|
+
string label
|
|
29
|
+
string field_type
|
|
34
30
|
string resource_type
|
|
35
|
-
|
|
31
|
+
boolean storefront_visible
|
|
36
32
|
boolean searchable
|
|
37
33
|
boolean sortable
|
|
38
34
|
}
|
|
39
35
|
|
|
40
|
-
|
|
41
|
-
string
|
|
42
|
-
|
|
43
|
-
string
|
|
36
|
+
CustomField {
|
|
37
|
+
string key
|
|
38
|
+
string label
|
|
39
|
+
string field_type
|
|
40
|
+
any value
|
|
44
41
|
}
|
|
45
42
|
```
|
|
46
43
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
subgraph Definition
|
|
50
|
-
A[MetafieldDefinition] --> B["namespace: properties"]
|
|
51
|
-
A --> C["key: manufacturer"]
|
|
52
|
-
A --> D["type: ShortText"]
|
|
53
|
-
A --> E["resource_type: Product"]
|
|
54
|
-
A --> F["display_on: both"]
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
subgraph Instance
|
|
58
|
-
G[Metafield] --> H["value: Wilson"]
|
|
59
|
-
G --> I["resource: Product #1"]
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
A -.->|"defines schema"| G
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
- **MetafieldDefinition** — the blueprint that defines the data type, target resource, and visibility
|
|
66
|
-
- **Metafield** — stores the actual value for a specific resource instance
|
|
67
|
-
|
|
68
|
-
## Data Types
|
|
44
|
+
- A **definition** is the declaration: "products have a Material, it is short text, shoppers may see it." You create it once.
|
|
45
|
+
- A **custom field** is one record's answer: this product's material is `100% Cotton`.
|
|
69
46
|
|
|
70
|
-
|
|
71
|
-
|------|-------------|----------------|
|
|
72
|
-
| Short Text | Brief text fields | SKU codes, brand names, tags |
|
|
73
|
-
| Long Text | Longer text content | Care instructions, notes |
|
|
74
|
-
| Rich Text | Formatted HTML content | Product descriptions with formatting |
|
|
75
|
-
| Number | Numeric values | Weight, quantity, ratings |
|
|
76
|
-
| Boolean | True/false flags | Is featured, requires signature |
|
|
77
|
-
| JSON | Structured data | Configuration, complex objects |
|
|
47
|
+
Because the definition carries the type and the label, the dashboard can build an editing form for it automatically, and your storefront gets a value it can trust the shape of.
|
|
78
48
|
|
|
79
|
-
##
|
|
49
|
+
## Field types
|
|
80
50
|
|
|
81
|
-
|
|
51
|
+
| Type | For | Example |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| `short_text` | A word or a line | `Wilson`, `100% Cotton` |
|
|
54
|
+
| `long_text` | A paragraph | Care instructions |
|
|
55
|
+
| `rich_text` | Formatted HTML | A spec sheet with lists and bold |
|
|
56
|
+
| `number` | A quantity or measure | `3.5`, `120` |
|
|
57
|
+
| `boolean` | A yes or no | Requires a signature |
|
|
58
|
+
| `json` | Structured data | A sizing chart |
|
|
82
59
|
|
|
83
|
-
|
|
84
|
-
|------------|:---------:|:---------:|----------|
|
|
85
|
-
| `both` | Yes | Yes | Public product specifications |
|
|
86
|
-
| `back_end` | No | Yes | Internal notes, integration IDs |
|
|
60
|
+
## Who can see it
|
|
87
61
|
|
|
88
|
-
|
|
62
|
+
`storefront_visible` decides whether a field ever leaves the back office:
|
|
89
63
|
|
|
90
|
-
|
|
64
|
+
| `storefront_visible` | Store API | Admin API | Use it for |
|
|
65
|
+
|---|:---:|:---:|---|
|
|
66
|
+
| `true` | Yes | Yes | Anything a shopper should see — materials, specs |
|
|
67
|
+
| `false` | No | Yes | Internal notes, supplier codes, sync IDs |
|
|
91
68
|
|
|
92
|
-
|
|
93
|
-
|------|--------|---------------------|
|
|
94
|
-
| `searchable` | Values are included in product text search | `short_text`, `long_text`, `number` |
|
|
95
|
-
| `sortable` | Listings can sort with `sort=cf_{namespace}_{key}` (prefix `-` for descending), e.g. `cf_custom_label` | `short_text`, `number` |
|
|
69
|
+
> **WARNING:** `storefront_visible: false` genuinely withholds the field from the Store API — it is not hidden in the response, it is absent. Treat it as the boundary between what a customer may read and what only staff may.
|
|
96
70
|
|
|
97
|
-
|
|
71
|
+
## Declaring a field
|
|
98
72
|
|
|
99
|
-
### Filtering
|
|
100
73
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
74
|
+
```typescript Admin SDK
|
|
75
|
+
const definition = await adminClient.customFieldDefinitions.create({
|
|
76
|
+
resource_type: 'Spree::Product',
|
|
77
|
+
namespace: 'properties',
|
|
78
|
+
key: 'material',
|
|
79
|
+
label: 'Material',
|
|
80
|
+
field_type: 'short_text',
|
|
81
|
+
storefront_visible: true,
|
|
82
|
+
searchable: true,
|
|
83
|
+
})
|
|
107
84
|
```
|
|
108
85
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
In the admin dashboard, these fields appear in the products table's column selector, Sort dropdown, and filter panel — hidden by default, so admins opt in per column.
|
|
121
|
-
|
|
122
|
-
## Supported Resources
|
|
86
|
+
```bash CLI
|
|
87
|
+
spree api post /custom_field_definitions -d '{
|
|
88
|
+
"resource_type": "Spree::Product",
|
|
89
|
+
"namespace": "properties",
|
|
90
|
+
"key": "material",
|
|
91
|
+
"label": "Material",
|
|
92
|
+
"field_type": "short_text",
|
|
93
|
+
"storefront_visible": true,
|
|
94
|
+
"searchable": true
|
|
95
|
+
}'
|
|
96
|
+
```
|
|
123
97
|
|
|
124
|
-
Metafields can be attached to most Spree resources including Products, Variants, Orders, Line Items, Taxons, Payments, Shipments, Gift Cards, Store Credits, and more.
|
|
125
98
|
|
|
126
|
-
|
|
99
|
+
`namespace` keeps groups of fields apart, so an integration's `product_id` never collides with yours. Together they form the key you'll see in responses: `properties.material`. Both are normalized to snake_case.
|
|
127
100
|
|
|
128
|
-
|
|
101
|
+
Definitions can also be managed in the dashboard under **Settings → Custom Fields**, which is where merchants usually add them.
|
|
129
102
|
|
|
130
|
-
|
|
103
|
+
## Setting and reading values
|
|
131
104
|
|
|
132
|
-
| Namespace | Example Keys | Purpose |
|
|
133
|
-
|-----------|-------------|---------|
|
|
134
|
-
| `properties` | `manufacturer`, `material`, `fit` | Product specifications |
|
|
135
|
-
| `shopify` | `product_id`, `variant_id` | Integration data |
|
|
136
|
-
| `flags` | `featured`, `requires_approval` | Feature flags |
|
|
137
|
-
| `custom` | `gift_message`, `delivery_notes` | Business-specific fields |
|
|
138
105
|
|
|
139
|
-
|
|
106
|
+
```typescript Admin SDK
|
|
107
|
+
// Set a value on a product
|
|
108
|
+
await adminClient.products.customFields.create('prod_xxx', {
|
|
109
|
+
custom_field_definition_id: definition.id,
|
|
110
|
+
value: '100% Cotton',
|
|
111
|
+
})
|
|
112
|
+
```
|
|
140
113
|
|
|
141
|
-
|
|
114
|
+
```bash CLI
|
|
115
|
+
spree api post /products/prod_xxx/custom_fields -d '{
|
|
116
|
+
"custom_field_definition_id": "cfdef_xxx",
|
|
117
|
+
"value": "100% Cotton"
|
|
118
|
+
}'
|
|
119
|
+
```
|
|
142
120
|
|
|
143
|
-
Metafields with `display_on` set to `both` are included in Store API responses when you [request the `custom_fields` expand](../../api-reference/store-api/relations.md):
|
|
144
121
|
|
|
145
|
-
|
|
122
|
+
Reading them from a storefront is an expand on whatever you already fetch:
|
|
146
123
|
|
|
147
124
|
|
|
148
125
|
```typescript Store SDK
|
|
@@ -150,44 +127,29 @@ const product = await client.products.get('spree-tote', {
|
|
|
150
127
|
expand: ['custom_fields'],
|
|
151
128
|
})
|
|
152
129
|
|
|
153
|
-
product.custom_fields?.forEach(field => {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
})
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
```typescript Admin SDK
|
|
162
|
-
const product = await adminClient.products.get('prod_86Rf07xd4z', {
|
|
163
|
-
expand: ['custom_fields'],
|
|
130
|
+
product.custom_fields?.forEach((field) => {
|
|
131
|
+
field.key // "properties.material"
|
|
132
|
+
field.label // "Material"
|
|
133
|
+
field.value // "100% Cotton"
|
|
134
|
+
field.field_type // "short_text"
|
|
164
135
|
})
|
|
165
136
|
```
|
|
166
137
|
|
|
167
138
|
```bash cURL
|
|
168
139
|
curl 'https://api.mystore.com/api/v3/store/products/spree-tote?expand=custom_fields' \
|
|
169
|
-
-H 'X-Spree-
|
|
140
|
+
-H 'X-Spree-API-Key: pk_xxx'
|
|
170
141
|
```
|
|
171
142
|
|
|
172
143
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
```json
|
|
144
|
+
```json Response
|
|
176
145
|
{
|
|
177
146
|
"id": "prod_86Rf07xd4z",
|
|
178
|
-
"name": "Spree
|
|
147
|
+
"name": "Spree Tote",
|
|
179
148
|
"custom_fields": [
|
|
180
149
|
{
|
|
181
150
|
"id": "cf_k5nR8xLq",
|
|
182
|
-
"label": "Manufacturer",
|
|
183
|
-
"key": "properties.manufacturer",
|
|
184
|
-
"field_type": "short_text",
|
|
185
|
-
"value": "Wilson"
|
|
186
|
-
},
|
|
187
|
-
{
|
|
188
|
-
"id": "cf_m3Rp9wXz",
|
|
189
|
-
"label": "Material",
|
|
190
151
|
"key": "properties.material",
|
|
152
|
+
"label": "Material",
|
|
191
153
|
"field_type": "short_text",
|
|
192
154
|
"value": "100% Cotton"
|
|
193
155
|
}
|
|
@@ -195,96 +157,57 @@ curl 'https://api.mystore.com/api/v3/store/products/spree-tote?expand=custom_fie
|
|
|
195
157
|
}
|
|
196
158
|
```
|
|
197
159
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
## Admin Management
|
|
201
|
-
|
|
202
|
-
### Managing Definitions
|
|
160
|
+
Each value arrives with its own label and type, so you can render a spec table straight from the array without hardcoding which fields exist.
|
|
203
161
|
|
|
204
|
-
|
|
162
|
+
## Searching, sorting and filtering
|
|
205
163
|
|
|
206
|
-
|
|
164
|
+
A custom field can take part in product listings — but only if you ask, because indexing everything by default would be wasteful.
|
|
207
165
|
|
|
166
|
+
| Flag | What it enables | Works with |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| `searchable` | The value is matched by product text search | `short_text`, `long_text`, `number` |
|
|
169
|
+
| `sortable` | Listings can be ordered by it | `short_text`, `number` |
|
|
208
170
|
|
|
209
|
-
|
|
210
|
-
import { createAdminClient } from '@spree/admin-sdk'
|
|
211
|
-
|
|
212
|
-
const client = createAdminClient({
|
|
213
|
-
baseUrl: 'https://store.example.com',
|
|
214
|
-
secretKey: 'sk_xxx',
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
const definition = await client.customFieldDefinitions.create({
|
|
218
|
-
resource_type: 'Spree::Product',
|
|
219
|
-
namespace: 'properties',
|
|
220
|
-
key: 'manufacturer',
|
|
221
|
-
label: 'Manufacturer',
|
|
222
|
-
field_type: 'short_text',
|
|
223
|
-
storefront_visible: true,
|
|
224
|
-
})
|
|
171
|
+
Setting either flag also makes the field **filterable**. Filter and sort with the field's `cf_` key, on both APIs:
|
|
225
172
|
|
|
226
|
-
await client.customFieldDefinitions.update(definition.id, { storefront_visible: false })
|
|
227
|
-
await client.customFieldDefinitions.delete(definition.id)
|
|
228
173
|
```
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
"resource_type": "Spree::Product",
|
|
233
|
-
"namespace": "properties",
|
|
234
|
-
"key": "manufacturer",
|
|
235
|
-
"label": "Manufacturer",
|
|
236
|
-
"field_type": "short_text",
|
|
237
|
-
"storefront_visible": true
|
|
238
|
-
}'
|
|
174
|
+
GET /api/v3/store/products?q[cf_properties_material_i_cont]=wool
|
|
175
|
+
GET /api/v3/store/products?sort=-cf_properties_weight
|
|
176
|
+
GET /api/v3/admin/products?q[cf_properties_weight_gteq]=3.5
|
|
239
177
|
```
|
|
240
178
|
|
|
179
|
+
| Field type | Comparisons available |
|
|
180
|
+
|---|---|
|
|
181
|
+
| `short_text`, `long_text` | `i_cont`, `cont`, `eq`, `not_eq`, `start`, `end`, `present`, `blank` |
|
|
182
|
+
| `number` | `eq`, `gt`, `gteq`, `lt`, `lteq`, `present`, `blank` |
|
|
241
183
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
When editing a resource (e.g., a product), metafields appear in a dedicated section. The admin panel automatically builds forms for all defined metafields.
|
|
245
|
-
|
|
246
|
-
To set a value programmatically, use the resource's nested `customFields` accessor (parent ID first):
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
```typescript Admin SDK
|
|
250
|
-
await client.products.customFields.create('prod_xxx', {
|
|
251
|
-
custom_field_definition_id: 'cfdef_xxx',
|
|
252
|
-
value: 'Wilson',
|
|
253
|
-
})
|
|
254
|
-
```
|
|
184
|
+
Use `i_cont` for case-insensitive matching. A comparison that doesn't suit the field's type is ignored rather than rejected, so a stale filter in a saved view can't break a page.
|
|
255
185
|
|
|
256
|
-
|
|
257
|
-
spree api post /products/prod_xxx/custom_fields -d '{
|
|
258
|
-
"custom_field_definition_id": "cfdef_xxx",
|
|
259
|
-
"value": "Wilson"
|
|
260
|
-
}'
|
|
261
|
-
```
|
|
186
|
+
> **INFO:** If you use Meilisearch, re-index after changing these flags so the new fields are picked up. Substring matching (`i_cont`, `cont`, `start`, `end`) is a database-provider feature; Meilisearch handles equality, ranges and presence. See [Search & Filtering](search-filtering.md).
|
|
262
187
|
|
|
188
|
+
In the dashboard, fields with these flags become available in the product table's column picker, sort menu and filter panel — off by default, so merchants opt in per column.
|
|
263
189
|
|
|
264
|
-
##
|
|
190
|
+
## What can carry custom fields
|
|
265
191
|
|
|
266
|
-
|
|
192
|
+
Most things you'd want to annotate: products, variants, orders, line items, customers, categories, payments, fulfillments, gift cards and store credits, among others.
|
|
267
193
|
|
|
268
|
-
|
|
269
|
-
|---------|-----------|----------|
|
|
270
|
-
| **Purpose** | Merchant-defined structured attributes | Developer escape hatch — integration IDs, sync state |
|
|
271
|
-
| **Schema** | Defined via MetafieldDefinitions | Schemaless JSON — no definition required |
|
|
272
|
-
| **Validation** | Type-specific (text, number, boolean, etc.) | None — accepts any JSON-serializable data |
|
|
273
|
-
| **Visibility** | Configurable (admin-only or public) | Write-only in Store API, readable in Admin API |
|
|
274
|
-
| **Admin UI** | Dedicated management forms | JSON preview |
|
|
275
|
-
| **Data Types** | 6 specific types | Any JSON value |
|
|
276
|
-
| **Organization** | Namespaced (`namespace.key`) | Flat key-value structure |
|
|
277
|
-
| **Queryable** | Via SQL joins, Ransack scopes, search providers | Via JSONB operators (PostgreSQL) |
|
|
194
|
+
## Custom fields or metadata?
|
|
278
195
|
|
|
279
|
-
|
|
196
|
+
Spree has two ways to store your own data, and they are not competing — they solve different problems.
|
|
280
197
|
|
|
281
|
-
|
|
198
|
+
| | Custom fields | [Metadata](../customization/metadata.md) |
|
|
199
|
+
|---|---|---|
|
|
200
|
+
| **For** | Data a merchant curates | Data your code keeps |
|
|
201
|
+
| **Shape** | Declared and typed | Any JSON, no declaration |
|
|
202
|
+
| **Dashboard** | Proper edit forms | Raw JSON |
|
|
203
|
+
| **Visibility** | Configurable per field | Never read by the Store API |
|
|
204
|
+
| **Searchable** | Yes, opt in | Not through the product search |
|
|
282
205
|
|
|
283
|
-
|
|
206
|
+
Put it simply: **custom fields are for people, metadata is for machines.** If a merchant should type it, define a custom field. If it is a sync token or an external ID that only your integration reads, use metadata.
|
|
284
207
|
|
|
285
|
-
## Related
|
|
208
|
+
## Related
|
|
286
209
|
|
|
287
|
-
- [Metadata](../customization/metadata.md) —
|
|
288
|
-
- [Products](products.md) —
|
|
289
|
-
- [
|
|
290
|
-
- [Admin SDK](../sdk/admin/resources.md) —
|
|
210
|
+
- [Metadata](../customization/metadata.md) — the machine-readable alternative
|
|
211
|
+
- [Products](products.md) — the most common place for custom fields
|
|
212
|
+
- [Search & Filtering](search-filtering.md) — how filters and search work
|
|
213
|
+
- [Admin SDK](../sdk/admin/resources.md) — managing definitions and values in TypeScript
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Order Totals
|
|
3
|
+
description: How an order total is built up from items, tax, discounts, delivery and fees — and how to render a summary that adds up.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
An order total is rarely just the sum of the item prices. Tax is added or already inside them, a promo code takes something off, delivery costs something, gift wrapping costs a bit more.
|
|
9
|
+
|
|
10
|
+
Spree records each of those as its own kind of row, and keeps a running total for each kind on the order itself. That means the summary block in your checkout is a handful of fields — you don't add anything up yourself.
|
|
11
|
+
|
|
12
|
+
```mermaid
|
|
13
|
+
flowchart TB
|
|
14
|
+
Items["Item total"] --> Total["Order total"]
|
|
15
|
+
Discounts["Discounts"] --> Total
|
|
16
|
+
Delivery["Delivery"] --> Total
|
|
17
|
+
Fees["Fees"] --> Total
|
|
18
|
+
Tax["Tax"] --> Total
|
|
19
|
+
|
|
20
|
+
style Total fill:#e8f5e9,stroke:#2e7d32
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## The totals
|
|
24
|
+
|
|
25
|
+
```typescript Store SDK
|
|
26
|
+
const order = await client.orders.get('or_xxx')
|
|
27
|
+
|
|
28
|
+
order.display_item_total // "$120.00" items, before tax and discounts
|
|
29
|
+
order.display_discount_total // "-$12.00" everything taken off
|
|
30
|
+
order.display_delivery_total // "$5.00" delivery
|
|
31
|
+
order.display_fee_total // "$2.50" surcharges
|
|
32
|
+
order.display_tax_total // "$22.60" all tax
|
|
33
|
+
order.display_total // "$138.10" what the customer pays
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
| Attribute | What it sums |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `item_total` | Line item prices, before tax and discounts |
|
|
39
|
+
| `discount_total` | Every [discount](discounts.md) |
|
|
40
|
+
| `delivery_total` | Delivery charges |
|
|
41
|
+
| `fee_total` | Every [fee](fees.md) |
|
|
42
|
+
| `tax_total` | All [tax](taxes.md) |
|
|
43
|
+
| `included_tax_total` | Tax already inside the displayed prices |
|
|
44
|
+
| `additional_tax_total` | Tax added on top |
|
|
45
|
+
| `total` | What the customer pays |
|
|
46
|
+
| `amount_due` | Still to pay, after gift cards and store credit |
|
|
47
|
+
|
|
48
|
+
## Two forms of every amount
|
|
49
|
+
|
|
50
|
+
Every total comes twice: `total` is the raw value, `display_total` is formatted for the order's currency.
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"total": "138.10",
|
|
55
|
+
"display_total": "$138.10"
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Render the `display_` one.** It knows the currency symbol, which side it goes on, and which separator that locale uses — `$138.10`, `138,10 €`, `¥138`. Formatting it yourself means reimplementing that, and getting it wrong for somebody.
|
|
60
|
+
|
|
61
|
+
> **NOTE:** Money is a **string**, never a number. JavaScript can't represent every decimal exactly: `0.1 + 0.2` gives `0.30000000000000004`, which is not something you want inside a price.
|
|
62
|
+
>
|
|
63
|
+
> If you must do arithmetic, use a decimal library or work in whole cents. Most of the time you don't need to — Spree already did it.
|
|
64
|
+
|
|
65
|
+
## The double-counting trap
|
|
66
|
+
|
|
67
|
+
> **WARNING:** If you build a subtotal yourself, include `additional_tax_total` only — never `included_tax_total`.
|
|
68
|
+
>
|
|
69
|
+
> Included tax is **already inside** the item prices. Adding it again charges the customer's eyes twice, and produces a summary that doesn't match the amount taken from their card. This is the single most common bug in a European storefront.
|
|
70
|
+
|
|
71
|
+
The two exist because the same order can carry both: VAT already inside the goods, and a separately-added tax on something else. `tax_total` covers both, which is why it's the safe field to display on its own line.
|
|
72
|
+
|
|
73
|
+
## Totals update themselves
|
|
74
|
+
|
|
75
|
+
Every change to a cart returns the whole cart, totals included:
|
|
76
|
+
|
|
77
|
+
```typescript Store SDK
|
|
78
|
+
const cart = await client.carts.items.create(cartId, {
|
|
79
|
+
variant_id: 'var_xxx',
|
|
80
|
+
quantity: 2,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
cart.display_total // already correct
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
There is never a second "recalculate" request to make, and never a moment where the summary on screen disagrees with what the server thinks. Adding an item, entering an address, applying a code — each response carries the new numbers.
|
|
87
|
+
|
|
88
|
+
The totals are worked out again at the moment the cart is completed, so a price that changed while the customer sat on the review page can't lead to the wrong charge.
|
|
89
|
+
|
|
90
|
+
## Once an order is placed
|
|
91
|
+
|
|
92
|
+
The rows stop being regenerated. Editing a placed order re-adds the rows it already has rather than starting over, so today's promotions and rates can't rewrite what a customer agreed to last week.
|
|
93
|
+
|
|
94
|
+
## Where each row attaches
|
|
95
|
+
|
|
96
|
+
The individual rows are there if you need them — an itemised invoice, a tax report:
|
|
97
|
+
|
|
98
|
+
- **[Tax lines](taxes.md)** → a line item, a fulfillment, or a fee
|
|
99
|
+
- **[Discounts](discounts.md)** → a line item or a fulfillment
|
|
100
|
+
- **[Fees](fees.md)** → a line item, a fulfillment, or the order itself
|
|
101
|
+
|
|
102
|
+
Each row also keeps a copy of where it 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.
|
|
103
|
+
|
|
104
|
+
## Related
|
|
105
|
+
|
|
106
|
+
- [Taxes](taxes.md) — how tax is worked out
|
|
107
|
+
- [Discounts](discounts.md) — money off
|
|
108
|
+
- [Fees](fees.md) — surcharges and duties
|
|
109
|
+
- [Carts](carts.md) — checkout and completion
|
|
110
|
+
- [Orders](orders.md) — the placed order
|
|
@@ -148,7 +148,7 @@ Where admin edits are allowed, totals are re-added from the order's existing row
|
|
|
148
148
|
|
|
149
149
|
## Money on an order
|
|
150
150
|
|
|
151
|
-
Tax, discounts and fees are kept as separate records, so you can ask what tax was charged without picking through a mixed list. See [
|
|
151
|
+
Tax, discounts and fees are kept as separate records, so you can ask what tax was charged without picking through a mixed list. See [Order totals](order-totals.md).
|
|
152
152
|
|
|
153
153
|
## Events
|
|
154
154
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Payments
|
|
3
|
+
description: Taking money — payment methods, payment sessions, saved cards, refunds, and how a payment finishes reliably even when a customer closes the tab.
|
|
3
4
|
---
|
|
4
5
|
|
|
5
6
|
## Overview
|
|
@@ -98,7 +99,7 @@ erDiagram
|
|
|
98
99
|
CreditCard }o--|| Customer : "belongs to"
|
|
99
100
|
StoreCredit }o--|| Customer : "belongs to"
|
|
100
101
|
Refund }o--|| Payment : "belongs to"
|
|
101
|
-
Refund }o
|
|
102
|
+
Refund }o--o| Return : "may come from"
|
|
102
103
|
```
|
|
103
104
|
|
|
104
105
|
**Key relationships:**
|
|
@@ -681,22 +682,18 @@ See [Events](events.md) for more details on subscribing to events.
|
|
|
681
682
|
|
|
682
683
|
- [Payments (Store SDK)](../sdk/store/payments.md) - SDK how-to for payment sessions, payments, and setup sessions
|
|
683
684
|
- [Build a Custom Payment Method](../how-to/custom-payment-method.md) - Step-by-step guide to creating your own payment gateway integration
|
|
684
|
-
- [Orders](orders.md) - Order
|
|
685
|
+
- [Orders](orders.md) - Order lifecycle, payment and fulfillment status
|
|
685
686
|
- [Checkout Customization](carts.md) - Customizing the checkout flow
|
|
686
687
|
- [Events](events.md) - Subscribe to payment events
|
|
687
688
|
|
|
688
|
-
##
|
|
689
|
+
## Two paths to a completed order
|
|
689
690
|
|
|
690
|
-
|
|
691
|
-
|---------|-------------|
|
|
692
|
-
| `Spree::Carts::Complete` | Completes the order — validates, processes payments (if not already done), advances state machine. Used by both the `POST /carts/:id/complete` endpoint and the webhook handler. |
|
|
693
|
-
| `Spree::Payments::HandleWebhook` | Processes a normalized webhook event — creates Payment, marks session completed, calls `Carts::Complete`. |
|
|
694
|
-
| `Spree::Payments::HandleWebhookJob` | Background job that wraps `HandleWebhook` — enqueued by the webhook controller for async processing. |
|
|
691
|
+
A payment can finish in two places, and both have to end at the same result.
|
|
695
692
|
|
|
696
|
-
|
|
693
|
+
The customer's browser confirms the payment and your storefront completes the cart. Or the provider's own webhook arrives first — sometimes seconds later, sometimes because the customer closed the tab mid-redirect.
|
|
697
694
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
695
|
+
Whichever arrives first completes the order; the other finds the work already done and does nothing. That is what makes a closed tab or a flaky connection recoverable rather than a lost sale with a real charge attached.
|
|
696
|
+
|
|
697
|
+
> **WARNING:** Never treat the browser returning from a redirect as proof of payment. The provider's webhook is the authoritative signal — a customer can close the tab, and a browser response can be forged.
|
|
698
|
+
|
|
699
|
+
Both paths can be replaced if your integration needs different behaviour — see [Dependencies](../customization/dependencies.md).
|