@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.
@@ -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: Products, variants, option types, images, prices, and categories
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 central model in Spree — every resource is scoped to a store
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 central model in Spree. Every resource — products, orders, markets, payment methods, taxonomies — is scoped to a store. Most setups use a single store, but Spree supports [multi-store](../multi-store/quickstart.md) configurations where each store operates independently with its own catalog, branding, and checkout.
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
- [Markets](markets.md) let you segment your store into geographic regions, each with its own currency, locale, and set of countries. For example, a single store can have:
51
+ Two different ways to split a store, often confused:
87
52
 
88
- - **North America** — USD, English, ships to US and Canada
89
- - **Europe** — EUR, German, ships to DE, FR, AT, NL
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
- See the [Markets](markets.md) guide for details.
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 has its own set of resources. This means products, orders, and promotions in one store are completely separate from another.
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 can be available in multiple stores |
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
- | [**Promotions**](promotions.md) | A promotion can apply to multiple stores |
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
- ## Multi-Store Setup
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
- To run multiple stores from a single Spree installation, see the [Multi-Store guide](../multi-store/quickstart.md). Each store gets its own domain, branding, catalog, and checkout — while sharing the same admin dashboard and infrastructure.
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: Localization, error handling, TypeScript types, and custom fetch
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: Learn how to write automated tests for the Brands feature
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, stores: [store])
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, stores: [store]) }
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, stores: [store]) }
195
- let!(:product2) { create(:product, brand: brand, stores: [store]) }
196
- let!(:other_product) { create(:product, stores: [store]) }
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, stores: [store], status: 'active') }
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, stores: [store], status: 'active')
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: This guide covers upgrading a Spree 5.4 application to Spree 5.5.
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 channels upgrade
40
+ ### Run the Channels upgrade
41
41
 
42
- Spree 5.5 promotes `Order#channel` from a free-text string column to a real `Spree::Channel` FK. **Run this rake task immediately after `db:migrate`** it seeds the channel rows your existing stores need and backfills `spree_orders.channel_id` from the legacy string column:
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 is idempotent — safe to re-run if it fails partway, and a no-op on stores/orders that have already been upgraded. It runs two sub-tasks in order:
50
+ The task runs four sub-tasks in order:
49
51
 
50
- 1. **`spree:channels:create_defaults`** — creates a default `online` channel for every store that doesn't already have one. New 5.5+ stores get this automatically via the `Store` after_create callback; this sub-task only matters for stores that already exist when you upgrade. The channel's after_create hook seeds the three default routing rules (Preferred Location → Minimize Splits → Default Location).
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
- 2. **`spree:channels:backfill_order_channel_ids`** scans each store's orders, creates one `Spree::Channel` per distinct legacy `channel` value (claiming `NULL`/blank rows under the `online` default channel), and writes `channel_id`.
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
- You can also run the two steps individually if you'd rather pace the upgrade:
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
- ```bash
57
- bin/rake spree:channels:create_defaults
58
- bin/rake spree:channels:backfill_order_channel_ids
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
- Orders modified after the upgrade auto-set `channel_id` via the model's `before_validation` callback, so the backfill is only strictly required for orders that aren't touched again post-upgrade but running it once at upgrade time avoids surprises later.
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 enough time has passed for everyone to have run the backfill — until then it's harmless DB ballast that lets the rake task be re-run if you need to.
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spree/docs",
3
- "version": "0.1.71",
3
+ "version": "0.1.73",
4
4
  "description": "Spree Commerce developer documentation for AI agents and local reference",
5
5
  "type": "module",
6
6
  "license": "CC-BY-4.0",