@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.
Files changed (31) hide show
  1. package/dist/api-reference/seller-api/errors.md +1 -1
  2. package/dist/api-reference/store.yaml +78 -91
  3. package/dist/developer/core-concepts/addresses.md +106 -198
  4. package/dist/developer/core-concepts/architecture.md +97 -126
  5. package/dist/developer/core-concepts/calculators.md +75 -252
  6. package/dist/developer/core-concepts/carts.md +1 -1
  7. package/dist/developer/core-concepts/channels.md +0 -4
  8. package/dist/developer/core-concepts/companies-and-catalogs.md +1 -1
  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 +9 -11
  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 +173 -19
  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/store-credits-gift-cards.md +0 -3
  26. package/dist/developer/core-concepts/taxes.md +125 -113
  27. package/dist/developer/core-concepts/translations.md +61 -68
  28. package/dist/developer/core-concepts/webhooks.md +25 -59
  29. package/dist/developer/how-to/custom-promotion.md +3 -3
  30. package/package.json +1 -1
  31. package/dist/developer/core-concepts/taxes-discounts-fees.md +0 -199
@@ -1,148 +1,125 @@
1
1
  ---
2
- title: Metafields
3
- description: Add custom structured data to products, orders, and other resources with type-safe metafields
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
- Metafields provide a flexible, type-safe system for adding custom structured attributes to Spree models. Unlike [metadata](../customization/metadata.md) which is simple JSON storage, metafields are schema-defined with strong typing, validation, and visibility controls.
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
- Use metafields for:
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
- - Product specifications (manufacturer, material, dimensions)
16
- - Custom business logic fields
17
- - Integration data from external systems
18
- - Order-specific custom attributes
14
+ ## Definitions and values
19
15
 
20
- ## Architecture
16
+ There are two halves, and it helps to keep them straight:
21
17
 
22
18
  ```mermaid
23
19
  erDiagram
24
- MetafieldDefinition ||--o{ Metafield : "defines schema for"
25
- Product ||--o{ Metafield : "has many"
26
- Variant ||--o{ Metafield : "has many"
27
- Order ||--o{ Metafield : "has many"
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
- MetafieldDefinition {
25
+ CustomFieldDefinition {
30
26
  string namespace
31
27
  string key
32
- string name
33
- string metafield_type
28
+ string label
29
+ string field_type
34
30
  string resource_type
35
- string display_on
31
+ boolean storefront_visible
36
32
  boolean searchable
37
33
  boolean sortable
38
34
  }
39
35
 
40
- Metafield {
41
- string type
42
- text value
43
- string resource_type
36
+ CustomField {
37
+ string key
38
+ string label
39
+ string field_type
40
+ any value
44
41
  }
45
42
  ```
46
43
 
47
- ```mermaid
48
- flowchart LR
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
- | Type | Description | Example Values |
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
- ## Visibility Control
49
+ ## Field types
80
50
 
81
- Metafields support two visibility levels via the `display_on` attribute:
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
- | Visibility | Store API | Admin API | Use Case |
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
- ## Search, sort & filter
62
+ `storefront_visible` decides whether a field ever leaves the back office:
89
63
 
90
- Product metafield definitions can opt into SearchProvider participation:
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
- | Flag | Effect | Allowed field types |
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
- Both default to `false`. Set them on the definition (Admin UI, Admin API, or seeds). For an existing catalog with Meilisearch, run `rake spree:search:reindex` after toggling flags so index settings and product documents pick up new `cf_*` fields.
71
+ ## Declaring a field
98
72
 
99
- ### Filtering
100
73
 
101
- Any definition with either flag set also becomes filterable on product listings, using its `cf_*` key as a Ransack predicate. The Store and Admin APIs accept the same keys and the same `sort` values:
102
-
103
- ```
104
- GET /api/v3/store/products?q[cf_custom_material_i_cont]=wool
105
- GET /api/v3/admin/products?q[cf_custom_weight_gteq]=3.5
106
- GET /api/v3/store/products?sort=-cf_custom_weight
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
- | Field type | Predicates |
110
- |------------|------------|
111
- | `short_text`, `long_text` | `i_cont`, `cont`, `eq`, `not_eq`, `start`, `end`, `present`, `blank` |
112
- | `number` | `eq`, `gt`, `gteq`, `lt`, `lteq`, `present`, `blank` |
113
-
114
- Use `i_cont` for case-insensitive matching — plain `cont` is case-sensitive on PostgreSQL.
115
-
116
- Predicates that don't match the definition's field type are ignored rather than rejected, as are keys matching no definition. Meilisearch supports the equality, range, and `present`/`blank` predicates; substring predicates (`i_cont`, `cont`, `start`, `end`) apply only to the database provider.
117
-
118
- CSV exports honor the same predicates, so exporting a filtered product list matches what the admin sees.
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
- > **INFO:** Custom resources can also support metafields. See the [Customization Quickstart](../customization/quickstart.md) for details.
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
- ## Namespaces
101
+ Definitions can also be managed in the dashboard under **Settings → Custom Fields**, which is where merchants usually add them.
129
102
 
130
- Namespaces organize metafields into logical groups and prevent key conflicts:
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
- > **INFO:** Namespace and key are automatically normalized to snake_case.
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
- ## Store API
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
- > **INFO:** Metafields in API are called Custom Fields as we plan to rename Metafields to Custom Fields completely in Spree 6.0.
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
- console.log(field.key) // "properties.manufacturer"
155
- console.log(field.label) // "Manufacturer"
156
- console.log(field.value) // "Wilson"
157
- console.log(field.field_type) // "short_text"
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-Api-Key: pk_xxx'
140
+ -H 'X-Spree-API-Key: pk_xxx'
170
141
  ```
171
142
 
172
143
 
173
- **Response:**
174
-
175
- ```json
144
+ ```json Response
176
145
  {
177
146
  "id": "prod_86Rf07xd4z",
178
- "name": "Spree T-Shirt",
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
- > **NOTE:** The `display_on` attribute is intentionally excluded from Store API responses for security.
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
- Navigate to **Settings → Metafield Definitions** in the Admin Panel to create and manage metafield definitions. Select the resource type, enter namespace and key, choose the data type, and set visibility.
162
+ ## Searching, sorting and filtering
205
163
 
206
- Definitions are also [managed via the Admin API](../../api-reference/admin-api/endpoints.md). `storefront_visible: true` is equivalent to `display_on: both` it exposes the field to the Store API:
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
- ```typescript Admin SDK
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
- ```bash CLI
231
- spree api post /custom_field_definitions -d '{
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
- ### Managing Values
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
- ```bash CLI
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
- ## Metafields vs Metadata
190
+ ## What can carry custom fields
265
191
 
266
- Spree has two permanent, complementary systems for custom data **metadata for machines, metafields for humans**. They serve different purposes and are not interchangeable. Neither is going away.
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
- | Feature | Metafields | [Metadata](../customization/metadata.md) |
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
- **Use Metafields** when you need type validation, visibility control, admin UI forms, or organized namespacing.
196
+ Spree has two ways to store your own data, and they are not competing — they solve different problems.
280
197
 
281
- **Use [Metadata](../customization/metadata.md)** for external system IDs, tracking attribution, syncing with integrations, or simple write-and-forget data that only backend systems need to read.
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
- > **WARNING:** Product Properties are deprecated and will be removed in Spree 6.0. For new projects, always use Metafields. For existing projects, plan to migrate using the [migration guide](/v5/developer/upgrades/5.1-to-5.2#migrate-to-metafields-or-keep-using-product-properties).
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 Documentation
208
+ ## Related
286
209
 
287
- - [Metadata](../customization/metadata.md) — Simple key-value metadata
288
- - [Products](products.md) — Product catalog
289
- - [Events](events.md) — Subscribe to metafield events
290
- - [Admin SDK](../sdk/admin/resources.md) — Manage definitions and values from TypeScript
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 [Taxes, Discounts & Fees](taxes-discounts-fees.md).
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--|| Reimbursement : "belongs to"
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 management and state machine
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
- ## Key Services
689
+ ## Two paths to a completed order
689
690
 
690
- | Service | Description |
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
- Both `Carts::Complete` and `HandleWebhook` are registered in `Spree::Dependencies` and can be replaced with custom implementations:
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
- ```ruby
699
- # config/initializers/spree.rb
700
- Spree::Dependencies.carts_complete_service = 'MyApp::CustomCartComplete'
701
- Spree::Dependencies.payments_handle_webhook_service = 'MyApp::CustomWebhookHandler'
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).