@spree/docs 0.1.306 → 0.1.308
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/admin-api/querying.md +17 -4
- package/dist/api-reference/store-api/querying.md +22 -10
- package/dist/api-reference/store.yaml +1715 -83
- package/dist/developer/cli/quickstart.md +14 -0
- package/dist/developer/core-concepts/external-references.md +226 -0
- package/dist/developer/core-concepts/metafields.md +3 -2
- package/dist/developer/core-concepts/search-filtering.md +85 -0
- package/dist/developer/how-to/build-a-b2b-store.md +1 -1
- package/dist/developer/providers/erp.md +1 -1
- package/dist/developer/providers/payouts.md +1 -1
- package/dist/developer/providers/pim.md +1 -1
- package/dist/developer/tutorial/model-and-api.md +1 -1
- package/dist/developer/upgrades/5.6-to-6.0.md +49 -0
- package/package.json +1 -1
|
@@ -249,6 +249,20 @@ spree generate migration AddPositionToSpreeBrands position:integer # Rails buil
|
|
|
249
249
|
|
|
250
250
|
`spree:api_resource` scaffolds the full v3 surface: model, migration, Store + Admin controllers and serializers, factory, controller specs, routes, and the `read_<resources>` / `write_<resources>` permissions that let staff roles and secret API keys reach the Admin API.
|
|
251
251
|
|
|
252
|
+
### `spree filters types`
|
|
253
|
+
|
|
254
|
+
Generate TypeScript declarations for your app's list filters, so the SDKs accept and autocomplete the filters and sort fields your app adds. It reads them from the running app and writes one file per run, so run it once for each app that uses an SDK, and again after you change a filter.
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
spree filters types --api store --out apps/storefront/src/types/spree-filters.d.ts # @spree/sdk
|
|
258
|
+
spree filters types --api admin --out apps/dashboard/src/types/spree-filters.d.ts # @spree/admin-sdk
|
|
259
|
+
spree filters types --api seller --out apps/seller-dashboard/src/types/spree-filters.d.ts # @spree/seller-sdk
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
`--from <file>` reads the tables from `bin/rails spree:api:filter_tables` output instead of the running app.
|
|
263
|
+
|
|
264
|
+
See [using your own filters from TypeScript](../core-concepts/search-filtering.md#using-your-own-filters-from-typescript).
|
|
265
|
+
|
|
252
266
|
### `spree migrate`
|
|
253
267
|
|
|
254
268
|
Install pending Spree migrations from gems, then run `db:migrate` — the canonical post-update sequence.
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: External References
|
|
3
|
+
description: Record the keys your ERP, PIM, DAM or CRM knows each record by, and address Spree records with those keys instead of Spree IDs.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
When Spree is connected to other systems, the same product lives in several places at once. Your ERP knows it as `MAT-100`, your PIM as `SKU-88412`, and Spree as `prod_86Rf07xd4z`.
|
|
9
|
+
|
|
10
|
+
External references are how Spree remembers those other keys. Each record can carry one key per system, and every integration can then find, create and update records using the key it already knows — without ever storing a Spree ID.
|
|
11
|
+
|
|
12
|
+
They solve three problems that come up in every integration:
|
|
13
|
+
|
|
14
|
+
- **Mapping.** Which Spree product is ERP item `MAT-100`?
|
|
15
|
+
- **Repeatable feeds.** A nightly import cannot know whether Spree has seen a row before. Sending the same row twice must update the record, not create a duplicate.
|
|
16
|
+
- **Several systems at once.** A PIM sync must never erase the key the ERP wrote.
|
|
17
|
+
|
|
18
|
+
## How references are stored
|
|
19
|
+
|
|
20
|
+
```mermaid
|
|
21
|
+
erDiagram
|
|
22
|
+
Product ||--o{ ExternalReference : "is known as"
|
|
23
|
+
Order ||--o{ ExternalReference : "is known as"
|
|
24
|
+
Company ||--o{ ExternalReference : "is known as"
|
|
25
|
+
|
|
26
|
+
ExternalReference {
|
|
27
|
+
string system
|
|
28
|
+
string external_id
|
|
29
|
+
json metadata
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
An external reference has three parts:
|
|
34
|
+
|
|
35
|
+
| Field | What it holds | Example |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `system` | Which system the key belongs to | `erp`, `pim`, `netsuite` |
|
|
38
|
+
| `external_id` | The key that system knows the record by | `MAT-100` |
|
|
39
|
+
| `metadata` | Optional bookkeeping for the integration (a version, an etag, when it last synced) | `{ "etag": "W/\"42\"" }` |
|
|
40
|
+
|
|
41
|
+
Two rules keep the mapping unambiguous, and the database enforces both:
|
|
42
|
+
|
|
43
|
+
- **One key per system per record.** A product has at most one `erp` key.
|
|
44
|
+
- **One record per key.** Within a store, `erp:MAT-100` names exactly one product.
|
|
45
|
+
|
|
46
|
+
### System keys
|
|
47
|
+
|
|
48
|
+
A system key is a short lowercase name made of letters, digits and underscores, such as `erp`, `akeneo` or `legacy_pim`. Spree lowercases and trims what you send, so `ERP` and `erp` are the same system.
|
|
49
|
+
|
|
50
|
+
The system does not have to be a connected [integration](../providers/overview.md). A nightly CSV from a legacy system that has no live connection still needs somewhere to record its keys. When a connector does exist, use the same name it uses, so the connector, its references and its settings page all agree.
|
|
51
|
+
|
|
52
|
+
## What can carry external references
|
|
53
|
+
|
|
54
|
+
| Record | Typical source of the key |
|
|
55
|
+
|---|---|
|
|
56
|
+
| Products | PIM or ERP item number |
|
|
57
|
+
| Variants | ERP or PIM SKU-level key |
|
|
58
|
+
| Categories | PIM category code |
|
|
59
|
+
| Media | DAM asset ID |
|
|
60
|
+
| Stock locations | ERP warehouse code |
|
|
61
|
+
| Stock levels | ERP inventory record |
|
|
62
|
+
| Orders | ERP sales order number, written back after the order is sent there |
|
|
63
|
+
| Companies | ERP or CRM account number (B2B) |
|
|
64
|
+
| Sellers | Payout provider account, recorded by the [payout provider](../providers/payouts.md) itself |
|
|
65
|
+
|
|
66
|
+
Customers are deliberately absent. A customer account can shop in more than one store, so a store-level key for it has no single owner. In B2B, the buyer's ERP identity belongs on their [company](companies.md) instead.
|
|
67
|
+
|
|
68
|
+
## Writing references
|
|
69
|
+
|
|
70
|
+
Send `external_references` with any create or update on the Admin API. It accepts the same map the API returns:
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
```typescript Admin SDK
|
|
74
|
+
const product = await adminClient.products.create({
|
|
75
|
+
name: 'Canvas Tote',
|
|
76
|
+
external_references: { erp: 'MAT-100', pim: 'SKU-88412' },
|
|
77
|
+
})
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```bash CLI
|
|
81
|
+
spree api post /products -d '{
|
|
82
|
+
"name": "Canvas Tote",
|
|
83
|
+
"external_references": { "erp": "MAT-100", "pim": "SKU-88412" }
|
|
84
|
+
}'
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
A list is accepted too, which some feeds find more natural:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"external_references": [
|
|
93
|
+
{ "system": "erp", "external_id": "MAT-100" },
|
|
94
|
+
{ "system": "pim", "external_id": "SKU-88412" }
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
What a write changes:
|
|
100
|
+
|
|
101
|
+
- **Systems you name are set.** A system the record has no key for gets one; a system it already has gets the new key.
|
|
102
|
+
- **Systems you don't name are left alone.** A PIM sending only `pim` never touches the `erp` key.
|
|
103
|
+
- **A blank key removes the reference.** Send `{ "erp": "" }` to unlink a record from the ERP.
|
|
104
|
+
|
|
105
|
+
## Reading references
|
|
106
|
+
|
|
107
|
+
Admin API responses for every record above except sellers include `external_references` as a flat map — the same shape you send:
|
|
108
|
+
|
|
109
|
+
```json Response
|
|
110
|
+
{
|
|
111
|
+
"id": "prod_86Rf07xd4z",
|
|
112
|
+
"name": "Canvas Tote",
|
|
113
|
+
"external_references": {
|
|
114
|
+
"erp": "MAT-100",
|
|
115
|
+
"pim": "SKU-88412"
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
External references never appear in the Store API. Which ERP a merchant runs, and the keys records have in it, is back-office information.
|
|
121
|
+
|
|
122
|
+
In the dashboard, references show in the **JSON** view on each record's detail page. There is no form for editing them: they are written by integrations and read by whoever is debugging a sync.
|
|
123
|
+
|
|
124
|
+
## Addressing records by your keys
|
|
125
|
+
|
|
126
|
+
For any record type listed in [What can carry external references](#what-can-carry-external-references), you can write `external:<system>:<key>` in the path instead of its Spree ID. An integration that only ever learned the ERP's key can then read, update and delete without first asking Spree for the record's ID.
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
```typescript Admin SDK
|
|
130
|
+
const product = await adminClient.products.get('external:erp:MAT-100')
|
|
131
|
+
|
|
132
|
+
await adminClient.products.update('external:erp:MAT-100', {
|
|
133
|
+
name: 'Canvas Tote — Natural',
|
|
134
|
+
})
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
```bash CLI
|
|
138
|
+
spree api get /products/external:erp:MAT-100
|
|
139
|
+
|
|
140
|
+
spree api patch /products/external:erp:MAT-100 -d '{
|
|
141
|
+
"name": "Canvas Tote — Natural"
|
|
142
|
+
}'
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
The lookup follows the same rules as a Spree ID. It only finds records in the current store, and an API key or admin user that cannot see the record gets a `404`, exactly as they would for its Spree ID.
|
|
147
|
+
|
|
148
|
+
> **WARNING:** Use this form only for keys made of letters, digits, dashes, underscores and colons. A key containing a slash or a dot cannot be part of a URL path; address that record by its Spree ID instead.
|
|
149
|
+
|
|
150
|
+
## Repeatable feeds
|
|
151
|
+
|
|
152
|
+
A create that names a key Spree already knows **updates that record instead of creating a new one**. A feed can therefore send every row on every run, and the result is the same as if it had sent each row once.
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
```typescript Admin SDK
|
|
156
|
+
// First run: creates the product.
|
|
157
|
+
// Every later run: updates the same product.
|
|
158
|
+
await adminClient.products.create({
|
|
159
|
+
name: 'Canvas Tote',
|
|
160
|
+
external_references: { erp: 'MAT-100' },
|
|
161
|
+
})
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
```bash CLI
|
|
165
|
+
spree api post /products -d '{
|
|
166
|
+
"name": "Canvas Tote",
|
|
167
|
+
"external_references": { "erp": "MAT-100" }
|
|
168
|
+
}'
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
The update needs the same permission an ordinary update would, so a key that may create records but not edit them cannot use a create to change one.
|
|
173
|
+
|
|
174
|
+
Image feeds work the same way. Media created from a URL with `external_references` is recorded under those keys, and a later run with the same key finds the existing image instead of adding a second copy.
|
|
175
|
+
|
|
176
|
+
## When a key is already taken
|
|
177
|
+
|
|
178
|
+
If an update names a key that already belongs to a **different** record, Spree keeps the record's other changes and answers with `422`:
|
|
179
|
+
|
|
180
|
+
```json Response
|
|
181
|
+
{
|
|
182
|
+
"error": {
|
|
183
|
+
"code": "conflicting_external_reference",
|
|
184
|
+
"message": "External has already been taken"
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
This usually means two records in Spree map to one record in the other system — for example, a product that was duplicated by hand. Remove the reference from one of them (send a blank key), then retry.
|
|
190
|
+
|
|
191
|
+
## Filtering companies by key
|
|
192
|
+
|
|
193
|
+
Companies can be filtered by their references, which helps when reconciling a list of accounts against an ERP:
|
|
194
|
+
|
|
195
|
+
```http
|
|
196
|
+
GET /api/v3/admin/companies?q[external_references_system_eq]=erp&q[external_references_external_id_eq]=ACME-1
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
For every other record, look it up by its key with the `external:<system>:<key>` path form above.
|
|
200
|
+
|
|
201
|
+
## References belong to a store
|
|
202
|
+
|
|
203
|
+
A reference is owned by the store its record belongs to. Two stores can each map `erp:MAT-100` to their own product without colliding, and a key from one store never finds a record in another. A feed that serves several stores sends its rows to each store separately.
|
|
204
|
+
|
|
205
|
+
## External references, custom fields or metadata?
|
|
206
|
+
|
|
207
|
+
All three can hold "an ID from another system", but only one is built for it.
|
|
208
|
+
|
|
209
|
+
| | External references | [Custom fields](metafields.md) | [Metadata](../customization/metadata.md) |
|
|
210
|
+
|---|---|---|---|
|
|
211
|
+
| **For** | A record's key in another system | Data a merchant curates | Any data your code keeps |
|
|
212
|
+
| **Unique per store** | Yes, enforced | No | No |
|
|
213
|
+
| **Find a record by it** | Yes, in the URL path | Through filters | No |
|
|
214
|
+
| **Create-or-update by it** | Yes | No | No |
|
|
215
|
+
| **Several systems at once** | Yes, one key each | One field per system | Your own structure |
|
|
216
|
+
| **Store API** | Never | Configurable | Never |
|
|
217
|
+
|
|
218
|
+
If an integration needs to find a record again by a key it owns, use an external reference. If a merchant should see and edit the value, use a custom field. Anything else your code needs to remember belongs in metadata.
|
|
219
|
+
|
|
220
|
+
## Related
|
|
221
|
+
|
|
222
|
+
- [ERP](../providers/erp.md) — feeding stock and orders from an ERP
|
|
223
|
+
- [PIM](../providers/pim.md) — syncing product content from a PIM
|
|
224
|
+
- [Imports & Exports](imports-exports.md) — file-based feeds
|
|
225
|
+
- [Companies](companies.md) — B2B accounts and their ERP identity
|
|
226
|
+
- [Admin SDK](../sdk/admin/resources.md) — the TypeScript client used in the examples
|
|
@@ -5,7 +5,7 @@ description: Add your own structured, typed data to products, orders and other r
|
|
|
5
5
|
|
|
6
6
|
## Overview
|
|
7
7
|
|
|
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.
|
|
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.
|
|
9
9
|
|
|
10
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.
|
|
11
11
|
|
|
@@ -209,11 +209,12 @@ Spree has two ways to store your own data, and they are not competing — they s
|
|
|
209
209
|
| **Visibility** | Configurable per field | Never read by the Store API |
|
|
210
210
|
| **Searchable** | Yes, opt in | Not through the product search |
|
|
211
211
|
|
|
212
|
-
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
|
|
212
|
+
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 that only your integration reads, use metadata. If it is the key another system knows the record by, use an [external reference](external-references.md).
|
|
213
213
|
|
|
214
214
|
## Related
|
|
215
215
|
|
|
216
216
|
- [Metadata](../customization/metadata.md) — the machine-readable alternative
|
|
217
|
+
- [External References](external-references.md) — keys from your ERP, PIM or CRM
|
|
217
218
|
- [Products](products.md) — the most common place for custom fields
|
|
218
219
|
- [Search & Filtering](search-filtering.md) — how filters and search work
|
|
219
220
|
- [Admin SDK](../sdk/admin/resources.md) — managing definitions and values in TypeScript
|
|
@@ -245,6 +245,91 @@ The Store API uses query parameters prefixed with `q[]` for filtering any resour
|
|
|
245
245
|
|
|
246
246
|
> **INFO:** Only attributes explicitly allowed by each resource can be used for filtering. Attempting to filter on unsupported fields will be silently ignored.
|
|
247
247
|
|
|
248
|
+
### Making a field filterable
|
|
249
|
+
|
|
250
|
+
A resource's filters come from its model's allowlists, and nowhere else. Spree generates its API reference and SDK types from them, so the [Querying](../../api-reference/admin-api/querying.md#which-filters-an-endpoint-accepts) reference lists what core declares. Filters your app adds take effect in the API immediately; to use them from TypeScript, declare them as shown below, since the published SDK types are generated from core alone.
|
|
251
|
+
|
|
252
|
+
```ruby server/config/initializers/spree.rb
|
|
253
|
+
Spree.ransack.add_attribute(Spree::Product, :erp_id)
|
|
254
|
+
Spree.ransack.add_association(Spree::Product, :brand)
|
|
255
|
+
Spree.ransack.add_scope(Spree::Product, :featured, type: 'boolean')
|
|
256
|
+
Spree.ransack.add_scope(Spree::Product, :by_brand, type: 'id')
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Give a model a `search` filter (`q[search]=term`) by naming the attributes it matches, partially and case-insensitively. Attributes of associated records are named with the association as a prefix:
|
|
260
|
+
|
|
261
|
+
```ruby server/app/models/spree/brand_decorator.rb
|
|
262
|
+
base.search_by :name, :code
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
`search` is offered to the Store and Seller APIs only when each attribute it matches is one that API may filter on. The API reference describes it from the attributes.
|
|
266
|
+
|
|
267
|
+
Declare the `type` of a scope's argument: `'boolean'` for a scope taking none (`q[featured]=true`), or `'text'`, `'id'`, `'decimal'`, `'integer'`, `'date'` or `'datetime'` for one value. A list of kinds (`%w[decimal decimal]`) means several arguments in order, and `{ list: 'id' }` means any number of them. A scope declared without a type is published as taking one text value.
|
|
268
|
+
|
|
269
|
+
Every entry is a filter any caller of that API can run. Keep back-office fields out of the Store API with `private_ransackable_attributes`, and add associations the storefront may filter through to `storefront_ransackable_associations`.
|
|
270
|
+
|
|
271
|
+
### Using your own filters from TypeScript
|
|
272
|
+
|
|
273
|
+
The SDKs type each `list()` call with the filters Spree itself declares. A filter your app adds works in the API at once, but TypeScript rejects it until it is declared, because the published types cannot know about it. There are three ways to use it.
|
|
274
|
+
|
|
275
|
+
**Generate the declarations (recommended).** This reads your app's filters, the ones Spree declares and the ones you added, and writes a declaration file for one SDK. Each frontend uses its own SDK, so generate a file for each app in your project, from the project root:
|
|
276
|
+
|
|
277
|
+
| App | SDK | `--api` |
|
|
278
|
+
|---|---|---|
|
|
279
|
+
| `apps/storefront` | `@spree/sdk` | `store` |
|
|
280
|
+
| `apps/dashboard` | `@spree/admin-sdk` | `admin` |
|
|
281
|
+
| `apps/seller-dashboard` (marketplaces) | `@spree/seller-sdk` | `seller` |
|
|
282
|
+
|
|
283
|
+
**Spree CLI:**
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
spree filters types --api store --out apps/storefront/src/types/spree-filters.d.ts
|
|
287
|
+
spree filters types --api admin --out apps/dashboard/src/types/spree-filters.d.ts
|
|
288
|
+
spree filters types --api seller --out apps/seller-dashboard/src/types/spree-filters.d.ts
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**Without CLI:**
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
cd server
|
|
295
|
+
bin/rails spree:api:filter_tables > ../filter-tables.json
|
|
296
|
+
cd ..
|
|
297
|
+
npx @spree/cli filters types --from filter-tables.json --api store --out apps/storefront/src/types/spree-filters.d.ts
|
|
298
|
+
npx @spree/cli filters types --from filter-tables.json --api admin --out apps/dashboard/src/types/spree-filters.d.ts
|
|
299
|
+
npx @spree/cli filters types --from filter-tables.json --api seller --out apps/seller-dashboard/src/types/spree-filters.d.ts
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
Run them again after you add or change a filter, and commit the generated files with the app code that uses them. Match `--api` to the app: a storefront file generated with `--api admin` compiles, but declares the Admin API's filters. After that, every `list()` call accepts and autocompletes your filters and sort fields, and a misspelled one is still a compile error.
|
|
304
|
+
|
|
305
|
+
**Send it unchecked.** Filters inside `q` are sent as given, without type checking or autocomplete:
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
await client.products.list({
|
|
309
|
+
name_cont: 'shirt',
|
|
310
|
+
q: { erp_id_eq: 'ERP-1' },
|
|
311
|
+
})
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
**Declare it by hand.** Each resource has an empty interface in the SDK (`ProductFilterExtensions`, `OrderFilterExtensions`, …) that is part of its filter type, and TypeScript [merges your declaration](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation) into it. The file must contain an `import` or `export`; without one, it replaces the SDK's types instead of adding to them:
|
|
315
|
+
|
|
316
|
+
```typescript src/types/spree.d.ts
|
|
317
|
+
export {}
|
|
318
|
+
|
|
319
|
+
declare module '@spree/sdk' {
|
|
320
|
+
interface ProductFilterExtensions {
|
|
321
|
+
erp_id_eq?: string
|
|
322
|
+
featured?: boolean
|
|
323
|
+
}
|
|
324
|
+
// Sort fields, as keys
|
|
325
|
+
interface ProductSortExtensions {
|
|
326
|
+
erp_id: true
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Name each filter the way the API receives it, with its predicate (`erp_id_eq`, `erp_id_in` for a list) or a scope by its name. Type amounts as decimal strings, counts as numbers and flags as booleans.
|
|
332
|
+
|
|
248
333
|
## Pagination
|
|
249
334
|
|
|
250
335
|
All list endpoints support pagination:
|
|
@@ -877,7 +877,7 @@ To add your own B2B cards, such as a credit account or an ERP reference, the com
|
|
|
877
877
|
The B2B building blocks are ordinary Spree resources, so the usual customization paths apply:
|
|
878
878
|
|
|
879
879
|
- **Events.** Companies, company invitations and catalogs publish lifecycle events, and orders publish `order.placed`. Subscribe to them to sync accounts and orders with an ERP or CRM. See [Events](../core-concepts/events.md).
|
|
880
|
-
- **External references.** Companies accept `external_references`, so an ERP can address a company by its own account number.
|
|
880
|
+
- **[External references](../core-concepts/external-references.md).** Companies accept `external_references`, so an ERP can address a company by its own account number.
|
|
881
881
|
- **Tax identifier validators.** Register a validator that checks numbers against a live registry.
|
|
882
882
|
- **Checkout requirements.** Register an extra requirement, such as "a cost center is required", and it appears in the cart's `requirements` next to the built-in ones. See [Checkout customization](../customization/checkout.md).
|
|
883
883
|
|
|
@@ -26,7 +26,7 @@ Two ways in, both writing stock movements rather than overwriting numbers — so
|
|
|
26
26
|
|
|
27
27
|
## Addressing records by your keys
|
|
28
28
|
|
|
29
|
-
The same product is known to your ERP and your PIM under different keys. External references are store-scoped mappings that let any feed address a Spree record as `external:<system>:<id>` — a create that names a key Spree already knows updates that record instead of failing.
|
|
29
|
+
The same product is known to your ERP and your PIM under different keys. External references are store-scoped mappings that let any feed address a Spree record as `external:<system>:<id>` — a create that names a key Spree already knows updates that record instead of failing. See [External References](../core-concepts/external-references.md).
|
|
30
30
|
|
|
31
31
|
## Checkout holds and failure policy
|
|
32
32
|
|
|
@@ -179,7 +179,7 @@ If your rails have no idempotency keys, look the movement up by that key (or by
|
|
|
179
179
|
|
|
180
180
|
## Seller accounts
|
|
181
181
|
|
|
182
|
-
A seller's account with your provider is stored as an [external reference](
|
|
182
|
+
A seller's account with your provider is stored as an [external reference](../core-concepts/external-references.md) filed under your `reference_system`, so a marketplace that migrates between providers keeps every account on record:
|
|
183
183
|
|
|
184
184
|
| Need | Call |
|
|
185
185
|
|---|---|
|
|
@@ -11,7 +11,7 @@ Your PIM owns product content — names, descriptions, attributes, associations,
|
|
|
11
11
|
|
|
12
12
|
## Product data feeds
|
|
13
13
|
|
|
14
|
-
Products, variants and prices flow in through the Admin API and CSV imports. Feeds address records by the keys your PIM already uses via external references (`external:<system>:<id>`), so an integration never has to store Spree IDs to update what it created.
|
|
14
|
+
Products, variants and prices flow in through the Admin API and CSV imports. Feeds address records by the keys your PIM already uses via [external references](../core-concepts/external-references.md) (`external:<system>:<id>`), so an integration never has to store Spree IDs to update what it created.
|
|
15
15
|
|
|
16
16
|
## The pricing provider
|
|
17
17
|
|
|
@@ -169,7 +169,7 @@ attribute(:brand_id) { |product| product.brand&.prefixed_id }
|
|
|
169
169
|
has_one :brand, serializer: Spree::Api::V3::BrandSerializer
|
|
170
170
|
```
|
|
171
171
|
|
|
172
|
-
The model has to allow filtering by it, or `?q[brand_id_eq]=…` is
|
|
172
|
+
The model has to allow filtering by it, or `?q[brand_id_eq]=…` is ignored:
|
|
173
173
|
|
|
174
174
|
```ruby server/app/models/spree/product_decorator.rb
|
|
175
175
|
base.whitelisted_ransackable_attributes |= %w[brand_id]
|
|
@@ -992,6 +992,55 @@ Both internal-note serializers now return the pair. Previously orders exposed on
|
|
|
992
992
|
|
|
993
993
|
Stored markup is also held to a narrower allowlist than 5.6 — see [the rich-text migration](#move-rich-text-out-of-action-text) for what is permitted and how to widen it.
|
|
994
994
|
|
|
995
|
+
## Upgrading a storefront
|
|
996
|
+
|
|
997
|
+
A storefront built on the Store API and `@spree/sdk`, such as the [Spree storefront](https://github.com/spree/storefront), needs `@spree/sdk` 2.0 and the changes below. Most are type errors that point at the line to change.
|
|
998
|
+
|
|
999
|
+
### List filters and sort are typed
|
|
1000
|
+
|
|
1001
|
+
Each `list()` method now takes the filters and sort fields its endpoint accepts. A misspelled filter, a filter the endpoint doesn't support, or an unknown sort field used to return the unfiltered list; it is now a compile error. `ProductListParams`, `CategoryListParams`, `CollectionListParams` and `OrderListParams` keep their names but no longer accept arbitrary keys.
|
|
1002
|
+
|
|
1003
|
+
- **A sort value read from the URL is a plain `string`.** Narrow it where you read it; the API still validates the value at runtime:
|
|
1004
|
+
|
|
1005
|
+
```ts src/lib/utils/product-query.ts
|
|
1006
|
+
import type { ProductListParams, ProductSort } from '@spree/sdk'
|
|
1007
|
+
|
|
1008
|
+
params.sort = filters.sortBy as ProductSort
|
|
1009
|
+
```
|
|
1010
|
+
|
|
1011
|
+
- **Filters your backend adds are compile errors until you declare them.** The published types list only the filters Spree itself declares. Any filter or scope your backend adds (through `Spree.ransack.add_attribute`, `add_scope` or a model decorator) still works in the API, but TypeScript rejects the key. Generate the declarations from your app, and run the command again whenever a filter changes:
|
|
1012
|
+
|
|
1013
|
+
```bash
|
|
1014
|
+
spree filters types --api store --out apps/storefront/src/types/spree-filters.d.ts
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
Generate one for the dashboard (`--api admin --out apps/dashboard/src/types/spree-filters.d.ts`) and the seller panel (`--api seller`) too, if they use your filters. For a one-off filter, send it inside `q` instead (`list({ q: { erp_id_eq: 'ERP-1' } })`), which is not type checked. See [using your own filters from TypeScript](../core-concepts/search-filtering.md#using-your-own-filters-from-typescript) for both, and for declaring filters by hand.
|
|
1018
|
+
|
|
1019
|
+
- **Amount filters take decimal strings.** `price_gte`, `price_lte` and the other money filters are typed as strings, like every money value in 6.0. Send `'20.00'`, not `20`.
|
|
1020
|
+
|
|
1021
|
+
- **Prefer `search`.** Products, categories and collections take `q[search]` (the SDK's `search` key), which matches more than `name_cont`. See [which filters an endpoint accepts](../../api-reference/store-api/querying.md#which-filters-an-endpoint-accepts).
|
|
1022
|
+
|
|
1023
|
+
### Completing a cart can return an order group
|
|
1024
|
+
|
|
1025
|
+
`carts.complete()` returns `Order | OrderGroup`. In a marketplace, a cart holding several sellers' goods divides into one order per seller and returns the group. Narrow the result with `isOrderGroup` before reading order fields:
|
|
1026
|
+
|
|
1027
|
+
```ts
|
|
1028
|
+
import { isOrderGroup } from '@spree/sdk'
|
|
1029
|
+
|
|
1030
|
+
const result = await client.carts.complete(cartId)
|
|
1031
|
+
const orders = isOrderGroup(result) ? result.orders : [result]
|
|
1032
|
+
```
|
|
1033
|
+
|
|
1034
|
+
### Other Store API changes to check
|
|
1035
|
+
|
|
1036
|
+
| Change | What to update | Details |
|
|
1037
|
+
|---|---|---|
|
|
1038
|
+
| Delivery requirement renamed | Match `delivery_method` (field) and `delivery_method_required` (code) instead of `shipping_method` | [Checkout without a state machine](#checkout-without-a-state-machine) |
|
|
1039
|
+
| Type values are short names | Compare `payment_source_type` with `credit_card`, and read custom field `field_type` | [Type values are short names](#type-values-are-short-names-not-class-names) |
|
|
1040
|
+
| Rich text comes in two shapes | Render `description_html` for markup; `description` is plain text | [Rich-text fields](#rich-text-fields-read-as-plain-text-plus-html) |
|
|
1041
|
+
| Payment source IDs | Stored `source_id` values change prefix from `ps_` to `psrc_` | [Payment source IDs](#payment-source-ids-use-the-psrc_-prefix) |
|
|
1042
|
+
| Completed carts are read-only | Read post-checkout data from the order, not the cart | [The Cart/Order split](#the-cartorder-split) |
|
|
1043
|
+
|
|
995
1044
|
## For extension authors
|
|
996
1045
|
|
|
997
1046
|
- **Don't reach for model business methods from services** — 6.0 code style writes behavior inline in service/workflow steps; models keep data, validations, predicates and persistence primitives. Extensions patching removed model methods (`finalize!` internals, updater hooks) should move to workflow hooks (`Spree.hooks.register('carts.complete.before_finalize') { |flow| ... }` — handlers receive the workflow instance) or event subscribers.
|