@spree/docs 0.1.71 → 0.1.73
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/developer/core-concepts/channels.md +167 -0
- package/dist/developer/core-concepts/products.md +148 -1
- package/dist/developer/core-concepts/stores.md +22 -52
- package/dist/developer/sdk/configuration.md +25 -1
- package/dist/developer/tutorial/testing.md +8 -8
- package/dist/developer/upgrades/5.4-to-5.5.md +24 -13
- package/package.json +1 -1
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Channels
|
|
3
|
+
description: Per-store distribution surfaces — online storefront, POS, marketplace, wholesale — each with its own product catalog and order attribution.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
import { Since } from '/snippets/since.mdx';
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
Channels segment a single [Store](stores.md) into distinct selling surfaces. A channel represents *where* an order originates from — the online storefront, an in-person point-of-sale till, a marketplace integration (Amazon, eBay), a B2B wholesale portal, a mobile app — and *which* subset of the store's products is available there.
|
|
12
|
+
|
|
13
|
+
Every store ships with one default channel named *Online Store*. You can add more from **Settings → Sales channels** in the admin dashboard.
|
|
14
|
+
|
|
15
|
+
## Channel Attributes
|
|
16
|
+
|
|
17
|
+
| Attribute | Description | Example |
|
|
18
|
+
|-----------|-------------|---------|
|
|
19
|
+
| `name` | Human-readable name, displayed in the admin and reports | `Point of Sale` |
|
|
20
|
+
| `code` | URL-safe slug, stable identifier sent via the `X-Spree-Channel` header | `pos` |
|
|
21
|
+
| `active` | When `false`, the channel stops accepting orders | `true` |
|
|
22
|
+
| `default` | Exactly one channel per store is the default. Used as a fallback when no channel header is present and as the auto-publish target for new products | `true` |
|
|
23
|
+
| `preferred_order_routing_strategy` | Optional per-channel override of the store's [Order Routing](shipments.md#order-routing) strategy | `Rules` |
|
|
24
|
+
|
|
25
|
+
`code` is normalized to a URL-safe slug on save — `POS` becomes `pos`, `Point of Sale!` becomes `point-of-sale`. Leaving `code` blank derives it from `name`.
|
|
26
|
+
|
|
27
|
+
## How Channels Work
|
|
28
|
+
|
|
29
|
+
### Resolution at request time
|
|
30
|
+
|
|
31
|
+
Every incoming Store API or storefront request resolves to a channel:
|
|
32
|
+
|
|
33
|
+
1. If the `X-Spree-Channel` header is present, the value is matched against `channels.code` — or `channels.id` when the value looks like a prefixed ID (`ch_…`) — scoped to the current store.
|
|
34
|
+
2. Otherwise, the store's default channel is used.
|
|
35
|
+
|
|
36
|
+
The resolved channel is then available to controllers, models, and serializers throughout the request.
|
|
37
|
+
|
|
38
|
+
### Selecting a channel from the Store SDK
|
|
39
|
+
|
|
40
|
+
The Store SDK sends `X-Spree-Channel` on every request when configured. The value can be either the channel `code` (merchant-meaningful, recommended) or the prefixed ID (`ch_…`).
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
```typescript SDK
|
|
44
|
+
// Client-level default
|
|
45
|
+
const client = createClient({
|
|
46
|
+
baseUrl: 'https://api.mystore.com',
|
|
47
|
+
publishableKey: 'pk_xxx',
|
|
48
|
+
channel: 'pos',
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// Sticky setter (mirrors setLocale / setCurrency / setCountry)
|
|
52
|
+
client.setChannel('wholesale')
|
|
53
|
+
|
|
54
|
+
// Per-request override
|
|
55
|
+
const products = await client.products.list({}, { channel: 'pos' })
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```bash cURL
|
|
59
|
+
curl 'https://api.mystore.com/api/v3/store/products' \
|
|
60
|
+
-H 'X-Spree-API-Key: pk_xxx' \
|
|
61
|
+
-H 'X-Spree-Channel: pos'
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
The Admin API does not consume `X-Spree-Channel` — admin endpoints return data across all channels for the current store. Filter by channel on the admin side via Ransack (`q[channel_id_eq]=ch_xxx` for orders, `q[channels_id_in][]=ch_xxx` for products).
|
|
66
|
+
|
|
67
|
+
### Product visibility
|
|
68
|
+
|
|
69
|
+
A product is visible on a channel only when it has a publication record joining the two. Each publication carries an optional window:
|
|
70
|
+
|
|
71
|
+
| Publication state | What customers see |
|
|
72
|
+
|---|---|
|
|
73
|
+
| No publication exists | Product is not on this channel — invisible |
|
|
74
|
+
| Publication has no dates set | Live now and indefinitely |
|
|
75
|
+
| `published_at` is in the future | Scheduled — not yet visible |
|
|
76
|
+
| `unpublished_at` is in the past | Hidden — was visible, now sunset |
|
|
77
|
+
| Within the window | Live |
|
|
78
|
+
|
|
79
|
+
Product status (`draft` / `active` / `archived`) is the **outer gate**: a Draft or Archived product is hidden on every channel regardless of its publication window. The dashboard's Publishing card renders this as a "Not available" badge on every channel row when status isn't `active`.
|
|
80
|
+
|
|
81
|
+
### Order attribution
|
|
82
|
+
|
|
83
|
+
Every order is attributed to one channel. The channel is set from the `X-Spree-Channel` header on cart creation, from the merchant's selection on the "New order" form, or defaults to the store's primary channel.
|
|
84
|
+
|
|
85
|
+
This attribution drives reporting (best-selling by channel, revenue per channel) and per-channel order routing — see [Order Routing](shipments.md#order-routing).
|
|
86
|
+
|
|
87
|
+
## Publishing Products on Channels
|
|
88
|
+
|
|
89
|
+
### Dashboard
|
|
90
|
+
|
|
91
|
+
The product edit page has a **Publishing** card with one row per channel the product is on. Click *Manage* to attach or detach channels via checkboxes. Each row expands into a per-channel schedule editor.
|
|
92
|
+
|
|
93
|
+
Bulk operations from the product list: *Add to sales channels…* and *Remove from sales channels…*.
|
|
94
|
+
|
|
95
|
+
### Admin API
|
|
96
|
+
|
|
97
|
+
Three endpoints cover the publishing surface:
|
|
98
|
+
|
|
99
|
+
| Endpoint | Use case |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `POST /api/v3/admin/channels/:id/add_products` | Publish one or more products on a specific channel |
|
|
102
|
+
| `POST /api/v3/admin/channels/:id/remove_products` | Unpublish products from a specific channel |
|
|
103
|
+
| `POST /api/v3/admin/products/bulk_add_to_channels` | Publish many products across many channels in a single request |
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
```typescript SDK
|
|
107
|
+
await adminClient.channels.addProducts('ch_xxx', {
|
|
108
|
+
product_ids: ['prod_aaa', 'prod_bbb'],
|
|
109
|
+
// Optional window — when omitted, existing schedules are preserved
|
|
110
|
+
published_at: '2026-07-01T00:00:00Z',
|
|
111
|
+
unpublished_at: '2026-12-31T23:59:59Z',
|
|
112
|
+
})
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```bash cURL
|
|
116
|
+
curl -X POST 'https://api.mystore.com/api/v3/admin/channels/ch_xxx/add_products' \
|
|
117
|
+
-H 'X-Spree-API-Key: sk_xxx' \
|
|
118
|
+
-H 'Content-Type: application/json' \
|
|
119
|
+
-d '{
|
|
120
|
+
"product_ids": ["prod_aaa", "prod_bbb"],
|
|
121
|
+
"published_at": "2026-07-01T00:00:00Z",
|
|
122
|
+
"unpublished_at": "2026-12-31T23:59:59Z"
|
|
123
|
+
}'
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
`channels.addProducts` is idempotent: re-publishing an already-published product is a no-op for its window unless `published_at` / `unpublished_at` are explicitly passed. Cross-store onboarding is allowed when the caller's key has update permission on the product.
|
|
128
|
+
|
|
129
|
+
For per-product updates, use `PATCH /api/v3/admin/products/:id` with a `product_publications` array:
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
```typescript SDK
|
|
133
|
+
await adminClient.products.update('prod_xxx', {
|
|
134
|
+
product_publications: [
|
|
135
|
+
{ channel_id: 'ch_online' },
|
|
136
|
+
{ channel_id: 'ch_pos', published_at: '2026-07-01T00:00:00Z' },
|
|
137
|
+
],
|
|
138
|
+
})
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
```bash cURL
|
|
142
|
+
curl -X PATCH 'https://api.mystore.com/api/v3/admin/products/prod_xxx' \
|
|
143
|
+
-H 'X-Spree-API-Key: sk_xxx' \
|
|
144
|
+
-H 'Content-Type: application/json' \
|
|
145
|
+
-d '{
|
|
146
|
+
"product_publications": [
|
|
147
|
+
{ "channel_id": "ch_online" },
|
|
148
|
+
{ "channel_id": "ch_pos", "published_at": "2026-07-01T00:00:00Z" }
|
|
149
|
+
]
|
|
150
|
+
}'
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
The write contract is **full-set**: the array represents the complete desired state. Channels absent from the payload are detached.
|
|
155
|
+
|
|
156
|
+
## Auto-Publish Behavior
|
|
157
|
+
|
|
158
|
+
- **Dashboard** — new products are auto-published on the store's default channel. The merchant can untick channels via the Publishing card post-create.
|
|
159
|
+
- **Admin API** — new products are **not** auto-published. The caller supplies `product_publications: [{ channel_id }]` on create, or calls `POST /admin/channels/:id/add_products` afterwards.
|
|
160
|
+
- **Sample data** (`bin/rake spree:load_sample_data`) — all loaded products are explicitly published on the default channel.
|
|
161
|
+
|
|
162
|
+
## Related Documentation
|
|
163
|
+
|
|
164
|
+
- [Stores](stores.md) — Channels belong to a store
|
|
165
|
+
- [Markets](markets.md) — Different from channels: markets segment geography/currency, channels segment selling surfaces
|
|
166
|
+
- [Products](products.md) — Product catalog and publication
|
|
167
|
+
- [Order Routing](shipments.md#order-routing) — Channels can override the store's routing strategy
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Products
|
|
3
|
-
description:
|
|
3
|
+
description: How Spree models products, variants, option types, images, prices, and categories — the building blocks of every catalog and storefront.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
import { Since } from '/snippets/since.mdx';
|
|
7
|
+
|
|
6
8
|
## Overview
|
|
7
9
|
|
|
8
10
|
A product represents something you sell. Each product has one or more **variants** — the actual purchasable items with their own SKU, price, and inventory. For example, a "T-Shirt" product might have variants for each size and color combination.
|
|
@@ -350,8 +352,153 @@ curl 'https://api.mystore.com/api/v3/store/categories/clothing/shirts/products?l
|
|
|
350
352
|
|
|
351
353
|
> **INFO:** Category `name` and `description` fields are translatable.
|
|
352
354
|
|
|
355
|
+
## Publications and Sales Channels
|
|
356
|
+
|
|
357
|
+
A product is visible on a [Channel](channels.md) only when a `ProductPublication` record joins the two. Publications carry an optional time window so a product can be scheduled to go live and come down without code or manual toggles.
|
|
358
|
+
|
|
359
|
+
| Publication state | What customers see |
|
|
360
|
+
|---|---|
|
|
361
|
+
| No publication exists | Product is not on this channel — invisible |
|
|
362
|
+
| Publication has no dates set | Live now and indefinitely |
|
|
363
|
+
| `published_at` is in the future | Scheduled — not yet visible |
|
|
364
|
+
| `unpublished_at` is in the past | Hidden — was visible, now sunset |
|
|
365
|
+
| Within the window | Live |
|
|
366
|
+
|
|
367
|
+
Product `status` (`draft` / `active` / `archived`) is the **outer gate**: a Draft or Archived product is hidden on every channel regardless of its publication window. Only `active` products consult publication state.
|
|
368
|
+
|
|
369
|
+
### Reading publications
|
|
370
|
+
|
|
371
|
+
Publications appear in the API under `product_publications` when expanded; the same data is available through the `channels` association as a flat list of joined channels.
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
```typescript SDK
|
|
375
|
+
const product = await adminClient.products.get('prod_abc', {
|
|
376
|
+
expand: ['product_publications', 'channels'],
|
|
377
|
+
})
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
```bash cURL
|
|
381
|
+
curl 'https://api.mystore.com/api/v3/admin/products/prod_abc?expand=product_publications,channels' \
|
|
382
|
+
-H 'X-Spree-API-Key: sk_xxx'
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
```json Response
|
|
387
|
+
{
|
|
388
|
+
"data": {
|
|
389
|
+
"id": "prod_abc",
|
|
390
|
+
"status": "active",
|
|
391
|
+
"channels": [
|
|
392
|
+
{ "id": "ch_online", "code": "online", "name": "Online Store" }
|
|
393
|
+
],
|
|
394
|
+
"product_publications": [
|
|
395
|
+
{
|
|
396
|
+
"id": "pp_xyz",
|
|
397
|
+
"channel_id": "ch_online",
|
|
398
|
+
"published_at": "2026-07-01T00:00:00Z",
|
|
399
|
+
"unpublished_at": null
|
|
400
|
+
}
|
|
401
|
+
]
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Writing publications
|
|
407
|
+
|
|
408
|
+
Two write surfaces serve different shapes:
|
|
409
|
+
|
|
410
|
+
- **Per-product, full-set** — `PATCH /api/v3/admin/products/{id}` with a `product_publications` array. The array represents the complete desired state; channels absent from the payload are detached.
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
```typescript SDK
|
|
415
|
+
await adminClient.products.update('prod_abc', {
|
|
416
|
+
product_publications: [
|
|
417
|
+
{ channel_id: 'ch_online' },
|
|
418
|
+
{ channel_id: 'ch_pos', published_at: '2026-07-01T00:00:00Z' },
|
|
419
|
+
],
|
|
420
|
+
})
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
```bash cURL
|
|
424
|
+
curl -X PATCH 'https://api.mystore.com/api/v3/admin/products/prod_abc' \
|
|
425
|
+
-H 'X-Spree-API-Key: sk_xxx' \
|
|
426
|
+
-H 'Content-Type: application/json' \
|
|
427
|
+
-d '{
|
|
428
|
+
"product_publications": [
|
|
429
|
+
{ "channel_id": "ch_online" },
|
|
430
|
+
{ "channel_id": "ch_pos", "published_at": "2026-07-01T00:00:00Z" }
|
|
431
|
+
]
|
|
432
|
+
}'
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
- **Per-channel, bulk** — `POST /api/v3/admin/channels/{id}/add_products` and `POST /api/v3/admin/channels/{id}/remove_products` for publishing or unpublishing many products at once. Idempotent: re-publishing an already-published product is a no-op for its window unless `published_at` / `unpublished_at` are explicitly passed.
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
```typescript SDK
|
|
442
|
+
await adminClient.channels.addProducts('ch_online', {
|
|
443
|
+
product_ids: ['prod_abc', 'prod_def'],
|
|
444
|
+
published_at: '2026-07-01T00:00:00Z',
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
await adminClient.channels.removeProducts('ch_online', {
|
|
448
|
+
product_ids: ['prod_abc'],
|
|
449
|
+
})
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
```bash cURL
|
|
453
|
+
curl -X POST 'https://api.mystore.com/api/v3/admin/channels/ch_online/add_products' \
|
|
454
|
+
-H 'X-Spree-API-Key: sk_xxx' \
|
|
455
|
+
-H 'Content-Type: application/json' \
|
|
456
|
+
-d '{
|
|
457
|
+
"product_ids": ["prod_abc", "prod_def"],
|
|
458
|
+
"published_at": "2026-07-01T00:00:00Z"
|
|
459
|
+
}'
|
|
460
|
+
|
|
461
|
+
curl -X POST 'https://api.mystore.com/api/v3/admin/channels/ch_online/remove_products' \
|
|
462
|
+
-H 'X-Spree-API-Key: sk_xxx' \
|
|
463
|
+
-H 'Content-Type: application/json' \
|
|
464
|
+
-d '{ "product_ids": ["prod_abc"] }'
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
The two surfaces converge on the same `spree_product_publications` table — pick whichever matches your call site.
|
|
470
|
+
|
|
471
|
+
### Listing products on a specific channel
|
|
472
|
+
|
|
473
|
+
Storefronts and `client.products.list()` calls return only products published on the resolved channel (live within the publication window, with the product itself `active`). To scope a Store SDK request to a non-default channel — e.g. a POS app querying for the POS catalog — set the channel `code` on the client or per-request:
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
```typescript SDK
|
|
477
|
+
// Client-level default
|
|
478
|
+
const client = createClient({ baseUrl, publishableKey, channel: 'pos' })
|
|
479
|
+
|
|
480
|
+
// Per-request override
|
|
481
|
+
const posProducts = await client.products.list({}, { channel: 'pos' })
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
```bash cURL
|
|
485
|
+
curl 'https://api.mystore.com/api/v3/store/products' \
|
|
486
|
+
-H 'X-Spree-API-Key: pk_xxx' \
|
|
487
|
+
-H 'X-Spree-Channel: pos'
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
For Admin API filtering across channels (back-office reports, admin UI lists), use Ransack instead: `q[channels_id_in][]=ch_xxx`. See [Sales Channels](channels.md) for the resolution rules.
|
|
492
|
+
|
|
493
|
+
### Auto-publish on the default channel
|
|
494
|
+
|
|
495
|
+
When a product is created via the dashboard, it is auto-published on the store's default channel (the only channel where `default = true`). The Admin API does **not** auto-publish — supply `product_publications: [{ channel_id }]` on create or call `add_products` afterwards.
|
|
496
|
+
|
|
497
|
+
See [Sales Channels](channels.md) for the full channel lifecycle, including default-channel resolution and the `X-Spree-Channel` header.
|
|
498
|
+
|
|
353
499
|
## Related Documentation
|
|
354
500
|
|
|
501
|
+
- [Sales Channels](channels.md) — Channels, publications, and order attribution
|
|
355
502
|
- [Pricing](pricing.md) — Price Lists, Price Rules, and market-specific pricing
|
|
356
503
|
- [Inventory](inventory.md) — Stock management and backorders
|
|
357
504
|
- [Media](media.md) — Image management
|
|
@@ -1,46 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Stores
|
|
3
|
-
description: The
|
|
3
|
+
description: The Store is Spree's top-level tenant — products, orders, channels, markets, and stock locations all belong to exactly one store.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## Overview
|
|
7
7
|
|
|
8
|
-
The Store is the
|
|
9
|
-
|
|
10
|
-
```mermaid
|
|
11
|
-
erDiagram
|
|
12
|
-
Store ||--o{ Market : "has many"
|
|
13
|
-
Store ||--o{ Order : "has many"
|
|
14
|
-
Store }o--o{ Product : "has many"
|
|
15
|
-
Store }o--o{ PaymentMethod : "has many"
|
|
16
|
-
Store }o--o{ ShippingMethod : "has many"
|
|
17
|
-
Store ||--o{ Taxonomy : "has many"
|
|
18
|
-
Store ||--o{ StockLocation : "has many"
|
|
19
|
-
|
|
20
|
-
Store {
|
|
21
|
-
string name
|
|
22
|
-
string code
|
|
23
|
-
string url
|
|
24
|
-
boolean default
|
|
25
|
-
string customer_support_email
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
Market {
|
|
29
|
-
string name
|
|
30
|
-
string currency
|
|
31
|
-
string default_locale
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
Product {
|
|
35
|
-
string name
|
|
36
|
-
string status
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
Order {
|
|
40
|
-
string number
|
|
41
|
-
string state
|
|
42
|
-
}
|
|
43
|
-
```
|
|
8
|
+
The Store is the top-level tenant in Spree. Every resource — products, orders, channels, markets, taxonomies, stock locations — belongs to exactly one store. A store owns its branding (logo, custom domain, mail-from address), its [channels](channels.md) (online, POS, wholesale, …), its [markets](markets.md) (region/currency/locale), and its catalog.
|
|
44
9
|
|
|
45
10
|
## Store Attributes
|
|
46
11
|
|
|
@@ -81,37 +46,42 @@ curl 'https://api.mystore.com/api/v3/store/store' \
|
|
|
81
46
|
```
|
|
82
47
|
|
|
83
48
|
|
|
84
|
-
## Markets
|
|
49
|
+
## Channels vs. Markets
|
|
85
50
|
|
|
86
|
-
|
|
51
|
+
Two different ways to split a store, often confused:
|
|
87
52
|
|
|
88
|
-
- **
|
|
89
|
-
- **
|
|
90
|
-
- **United Kingdom** — GBP, English, ships to GB
|
|
53
|
+
- **[Sales Channels](channels.md)** segment **selling surfaces** — Online Store, POS, Wholesale, marketplace integrations. They control product visibility, order attribution, and per-channel routing rules.
|
|
54
|
+
- **[Markets](markets.md)** segment **geography and currency** — North America (USD/en), Europe (EUR/de), UK (GBP/en). They control which currency, locale, and tax rules apply to a given customer.
|
|
91
55
|
|
|
92
|
-
|
|
56
|
+
A single Online Store channel can serve multiple markets (one storefront → many regions). Conversely, POS and Online channels can share the same market (same currency/locale, different selling surfaces).
|
|
93
57
|
|
|
94
58
|
## Store Resources
|
|
95
59
|
|
|
96
|
-
Each store
|
|
60
|
+
Each store owns its own resources. Products, orders, channels, markets, and taxonomies in one store are independent from another.
|
|
97
61
|
|
|
98
62
|
| Resource | Relationship |
|
|
99
63
|
|----------|-------------|
|
|
64
|
+
| [**Channels**](channels.md) | A store has many channels (Online Store, POS, Wholesale, …). One is the default. |
|
|
100
65
|
| [**Markets**](markets.md) | A store has many markets, each defining a geographic region with its own currency and locale |
|
|
101
|
-
| [**Orders**](orders.md) | An order belongs to one store |
|
|
102
|
-
| [**Products**](products.md) | A product
|
|
103
|
-
| [**Payment Methods**](payments.md) | A payment method can be available in multiple stores |
|
|
104
|
-
| [**Shipping Methods**](shipments.md) | A shipping method can be available in multiple stores |
|
|
66
|
+
| [**Orders**](orders.md) | An order belongs to one store and one channel |
|
|
67
|
+
| [**Products**](products.md) | A product belongs to one store. Its visibility across channels is controlled by [publications](channels.md#publishing-products-on-channels). |
|
|
105
68
|
| [**Taxonomies**](products.md#taxons-and-taxonomies) | A taxonomy belongs to one store |
|
|
106
|
-
| [**
|
|
69
|
+
| [**Payment Methods**](payments.md) | A payment method belongs to one store |
|
|
70
|
+
| [**Shipping Methods**](shipments.md) | A shipping method belongs to one store |
|
|
71
|
+
| [**Promotions**](promotions.md) | A promotion belongs to one store |
|
|
72
|
+
|
|
73
|
+
## Running Multiple Storefronts
|
|
74
|
+
|
|
75
|
+
If you need one Spree backend to serve **multiple distinct merchant brands** — different domains, different catalogs, different admin teams — there are two patterns:
|
|
107
76
|
|
|
108
|
-
|
|
77
|
+
- **Multiple channels under one store** (recommended for most cases) — model each storefront as a Sales Channel. Products are scoped per-channel via publications, orders carry the channel that originated them, and routing/pricing can differ per channel. This is the supported pattern in core Spree 5.5+.
|
|
78
|
+
- **Multiple stores in one app** — the legacy multi-store pattern, where each store is fully independent (catalog, admin, branding). In Spree 5.5+ this requires the `spree_multi_store` extension. Core Spree treats each product as belonging to a single store.
|
|
109
79
|
|
|
110
|
-
|
|
80
|
+
For most multi-brand operations, the channel pattern is simpler and more flexible. Reach for `spree_multi_store` only when stores need genuinely independent catalogs, branding, and admin teams.
|
|
111
81
|
|
|
112
82
|
## Related Documentation
|
|
113
83
|
|
|
84
|
+
- [Channels](channels.md) — Selling surfaces (Online, POS, Wholesale, …)
|
|
114
85
|
- [Markets](markets.md) — Multi-region commerce within a store
|
|
115
86
|
- [Products](products.md) — Product catalog
|
|
116
87
|
- [Orders](orders.md) — Order management and checkout
|
|
117
|
-
- [Multi-Store](../multi-store/quickstart.md) — Running multiple stores
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Configuration
|
|
3
|
-
description:
|
|
3
|
+
description: Configure the Spree JavaScript SDK — localization, currency headers, error handling, TypeScript types, and a custom fetch implementation.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## Localization & Currency
|
|
@@ -27,6 +27,30 @@ const category = await client.categories.get('clothing/shirts', {
|
|
|
27
27
|
})
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
import { Since } from '/snippets/since.mdx';
|
|
31
|
+
|
|
32
|
+
## Sales Channels
|
|
33
|
+
|
|
34
|
+
Select which [sales channel](../core-concepts/channels.md) scopes a request. The SDK sends the value as the `X-Spree-Channel` header. Pass either the channel `code` (e.g. `online`, `pos`, `wholesale`) or the prefixed ID (`ch_…`) — `code` is preferred since it's merchant-meaningful and stable across environments. When omitted, the store's default channel is used.
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
// Client-level default — every request runs against the POS channel
|
|
38
|
+
const client = createClient({
|
|
39
|
+
baseUrl: 'https://api.mystore.com',
|
|
40
|
+
publishableKey: 'pk_xxx',
|
|
41
|
+
channel: 'pos',
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// Sticky setter — switch channels mid-session (e.g. after the user
|
|
45
|
+
// changes storefront context)
|
|
46
|
+
client.setChannel('wholesale')
|
|
47
|
+
|
|
48
|
+
// Per-request override — one request only
|
|
49
|
+
const products = await client.products.list({}, { channel: 'pos' })
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The channel scopes product visibility (only products published on that channel are returned), pricing fallback (per-channel pricing rules), and order attribution (a cart created with `channel: 'pos'` gets `order.channel_id` set to the POS channel).
|
|
53
|
+
|
|
30
54
|
## Error Handling
|
|
31
55
|
|
|
32
56
|
```typescript
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Testing
|
|
3
|
-
description:
|
|
3
|
+
description: Write automated tests for the Brands tutorial feature using RSpec, Factory Bot, and Capybara, plus helpers from the spree_dev_tools gem.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Automated testing is a crucial part of the development process. It helps you ensure that your code works as expected and catches bugs early.
|
|
@@ -77,7 +77,7 @@ FactoryBot.define do
|
|
|
77
77
|
|
|
78
78
|
after(:create) do |brand, evaluator|
|
|
79
79
|
store = evaluator.store || create(:store)
|
|
80
|
-
create_list(:product, evaluator.products_count, brand: brand
|
|
80
|
+
create_list(:product, evaluator.products_count, brand: brand)
|
|
81
81
|
end
|
|
82
82
|
end
|
|
83
83
|
end
|
|
@@ -174,7 +174,7 @@ require 'rails_helper'
|
|
|
174
174
|
RSpec.describe 'Spree::Product brand association' do
|
|
175
175
|
let(:store) { @default_store }
|
|
176
176
|
let(:brand) { create(:brand) }
|
|
177
|
-
let(:product) { create(:product
|
|
177
|
+
let(:product) { create(:product) }
|
|
178
178
|
|
|
179
179
|
describe 'brand association' do
|
|
180
180
|
it 'can be assigned a brand' do
|
|
@@ -191,9 +191,9 @@ RSpec.describe 'Spree::Product brand association' do
|
|
|
191
191
|
end
|
|
192
192
|
|
|
193
193
|
describe 'brand.products' do
|
|
194
|
-
let!(:product1) { create(:product, brand: brand
|
|
195
|
-
let!(:product2) { create(:product, brand: brand
|
|
196
|
-
let!(:other_product) { create(:product
|
|
194
|
+
let!(:product1) { create(:product, brand: brand) }
|
|
195
|
+
let!(:product2) { create(:product, brand: brand) }
|
|
196
|
+
let!(:other_product) { create(:product) }
|
|
197
197
|
|
|
198
198
|
it 'returns products for the brand' do
|
|
199
199
|
expect(brand.products).to contain_exactly(product1, product2)
|
|
@@ -327,7 +327,7 @@ RSpec.describe Spree::Api::V3::Store::ProductsController, type: :controller do
|
|
|
327
327
|
include_context 'API v3 Store'
|
|
328
328
|
|
|
329
329
|
let(:brand) { create(:brand, name: 'Nike') }
|
|
330
|
-
let!(:product) { create(:product, brand: brand,
|
|
330
|
+
let!(:product) { create(:product, brand: brand, status: 'active') }
|
|
331
331
|
|
|
332
332
|
before do
|
|
333
333
|
request.headers['X-Spree-Api-Key'] = api_key.token
|
|
@@ -357,7 +357,7 @@ RSpec.describe Spree::Api::V3::Store::ProductsController, type: :controller do
|
|
|
357
357
|
|
|
358
358
|
describe 'GET #index' do
|
|
359
359
|
it 'filters products by brand_id' do
|
|
360
|
-
other_product = create(:product,
|
|
360
|
+
other_product = create(:product, status: 'active')
|
|
361
361
|
|
|
362
362
|
get :index, params: { q: { brand_id_eq: brand.id } }
|
|
363
363
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Upgrading to Spree 5.5
|
|
3
|
-
description:
|
|
3
|
+
description: Step-by-step guide to upgrading a Spree 5.4 application to Spree 5.5, including gem updates, migrations, and breaking changes to review.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
> **INFO:** Before proceeding to upgrade, please ensure you're at [Spree 5.4](5.3-to-5.4.md).
|
|
@@ -37,30 +37,41 @@ bin/rails spree:media:migrate_master_images_to_product_media BATCH_SIZE=1000
|
|
|
37
37
|
|
|
38
38
|
> **WARNING:** Run the task locally and on production. It does not block storefront rendering — new uploads attach to the product immediately — but until the enqueued jobs finish, old assets remain pinned to variants.
|
|
39
39
|
|
|
40
|
-
### Run the
|
|
40
|
+
### Run the Channels upgrade
|
|
41
41
|
|
|
42
|
-
Spree 5.5
|
|
42
|
+
Spree 5.5 introduces [Sales Channels](../core-concepts/channels.md) — a per-store distribution surface (online storefront, POS, marketplace integration, wholesale portal). Products are published to a channel via the new `spree_product_publications` join table, and orders are attributed to a channel via `spree_orders.channel_id`.
|
|
43
|
+
|
|
44
|
+
The migrations add a `default` boolean on `spree_channels`, a `store_id` column on `spree_products`, and create the new `spree_product_publications` table — **but they do not seed default channels, attach existing products to a store, or backfill order channels**. That work is done by an idempotent rake task you must run after `db:migrate`:
|
|
43
45
|
|
|
44
46
|
```bash
|
|
45
47
|
bin/rake spree:channels:upgrade
|
|
46
48
|
```
|
|
47
49
|
|
|
48
|
-
The task
|
|
50
|
+
The task runs four sub-tasks in order:
|
|
49
51
|
|
|
50
|
-
1.
|
|
52
|
+
1. `spree:channels:create_defaults` — creates the default "Online Store" channel for every existing store (via `Store#ensure_default_channel`).
|
|
53
|
+
2. `spree:upgrade:populate_publications` — for every product that doesn't yet have a `store_id`, picks a "home" store from the legacy `spree_products_stores` join (preferring the store flagged `default: true`, otherwise the earliest row), sets `spree_products.store_id`, and creates a `spree_product_publications` row on each attached store's default channel. Runs in a transaction per product so a partial failure leaves nothing half-applied.
|
|
54
|
+
3. `spree:channels:backfill_order_channel_ids` — sets `spree_orders.channel_id` from the legacy `spree_orders.channel` string column. Unknown codes auto-create a new channel under that store. NULL/blank values map to the default channel.
|
|
55
|
+
4. `spree:channels:backfill_product_publication_dates` — copies the deprecated `Product.available_on` and `Product.discontinue_on` columns into each publication's `published_at` / `unpublished_at` (only where the publication's date is currently NULL).
|
|
51
56
|
|
|
52
|
-
|
|
57
|
+
The task is fully idempotent — safe to re-run if it fails partway, and a no-op on stores/products/orders that have already been upgraded.
|
|
53
58
|
|
|
54
|
-
|
|
59
|
+
> **WARNING:** Until `spree:channels:upgrade` runs, every product has `store_id IS NULL` and is invisible to `Product.for_store(store)`. The admin product list, storefront catalog, and search indexer all return empty. Run the task immediately after `db:migrate`.
|
|
55
60
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
#### Multi-store catalogs
|
|
62
|
+
|
|
63
|
+
If you have products attached to multiple stores via the legacy `spree_products_stores` join, the `populate_publications` task picks **one** "home" store per product and creates publications on every store's default channel. The `spree_products_stores` table is **kept** as legacy compat surface — the upcoming `spree_multi_store` extension restores the full `Product has_many :stores` association on top of it.
|
|
64
|
+
|
|
65
|
+
For single-store deployments this is invisible; you can move on without touching `spree_products_stores` again.
|
|
66
|
+
|
|
67
|
+
#### Behavioral changes
|
|
60
68
|
|
|
61
|
-
|
|
69
|
+
- Newly-created products in **the dashboard SPA** are auto-published on the store's default channel; the merchant can untick channels post-create via the Publishing card.
|
|
70
|
+
- Newly-created products via the **Admin API** are NOT auto-published — the caller must supply `product_publications: [{ channel_id }]` on create or use `POST /api/v3/admin/channels/:id/add_products` afterwards.
|
|
71
|
+
- `Product.available_on=` and `Product.discontinue_on=` setters emit deprecation warnings and now write to every per-channel publication's `published_at` / `unpublished_at`. Reading these attributes on a product prefers the current-channel publication's value over the legacy column.
|
|
72
|
+
- `Spree::Channel#add_products(product_ids)` is idempotent and **preserves existing publication windows** when called without `published_at`/`unpublished_at` kwargs.
|
|
62
73
|
|
|
63
|
-
The legacy `channel` string column is **kept** on `spree_orders` and ignored by ActiveRecord (`Spree::Order` declares it in `ignored_columns`). It will be dropped in a later Spree release once
|
|
74
|
+
Orders modified after the upgrade auto-set `channel_id` via the model's `before_validation :ensure_channel_presence` callback, so `backfill_order_channel_ids` is only strictly required for orders that aren't touched again post-upgrade — but running it at upgrade time avoids surprises later. The legacy `channel` string column is **kept** on `spree_orders` and ignored by ActiveRecord (`Spree::Order` declares it in `ignored_columns`). It will be dropped in a later Spree release once everyone has had a chance to run the backfill.
|
|
64
75
|
|
|
65
76
|
### Schedule the Stock Reservations expiry job
|
|
66
77
|
|