@spree/docs 0.1.93 → 0.1.95

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/api-reference/admin-api/authentication.md +8 -7
  2. package/dist/api-reference/admin-api/endpoints.md +348 -0
  3. package/dist/api-reference/admin-api/errors.md +2 -0
  4. package/dist/api-reference/admin-api/introduction.md +11 -0
  5. package/dist/api-reference/store.yaml +243 -232
  6. package/dist/developer/agentic/overview.md +1 -1
  7. package/dist/developer/cli/admin-api.md +146 -0
  8. package/dist/developer/cli/quickstart.md +40 -5
  9. package/dist/developer/core-concepts/addresses.md +32 -16
  10. package/dist/developer/core-concepts/adjustments.md +11 -5
  11. package/dist/developer/core-concepts/architecture.md +8 -8
  12. package/dist/developer/core-concepts/calculators.md +31 -51
  13. package/dist/developer/core-concepts/channels.md +13 -6
  14. package/dist/developer/core-concepts/customers.md +47 -23
  15. package/dist/developer/core-concepts/events.md +22 -17
  16. package/dist/developer/core-concepts/imports-exports.md +69 -14
  17. package/dist/developer/core-concepts/inventory.md +79 -1
  18. package/dist/developer/core-concepts/markets.md +64 -20
  19. package/dist/developer/core-concepts/media.md +43 -6
  20. package/dist/developer/core-concepts/metafields.md +76 -13
  21. package/dist/developer/core-concepts/orders.md +95 -17
  22. package/dist/developer/core-concepts/payments.md +14 -13
  23. package/dist/developer/core-concepts/pricing.md +95 -9
  24. package/dist/developer/core-concepts/products.md +192 -26
  25. package/dist/developer/core-concepts/promotions.md +61 -4
  26. package/dist/developer/core-concepts/reports.md +4 -2
  27. package/dist/developer/core-concepts/search-filtering.md +82 -32
  28. package/dist/developer/core-concepts/shipments.md +16 -13
  29. package/dist/developer/core-concepts/slugs.md +20 -11
  30. package/dist/developer/core-concepts/staff-roles.md +51 -1
  31. package/dist/developer/core-concepts/store-credits-gift-cards.md +90 -9
  32. package/dist/developer/core-concepts/stores.md +16 -14
  33. package/dist/developer/core-concepts/taxes.md +28 -0
  34. package/dist/developer/core-concepts/translations.md +16 -7
  35. package/dist/developer/core-concepts/users.md +13 -9
  36. package/dist/developer/core-concepts/webhooks.md +95 -64
  37. package/dist/developer/getting-started/quickstart.md +20 -0
  38. package/dist/developer/how-to/custom-api-authentication.md +103 -23
  39. package/dist/developer/multi-store/quickstart.md +1 -1
  40. package/dist/developer/sdk/admin/authentication.md +1 -1
  41. package/dist/developer/sdk/admin/resources.md +2 -0
  42. package/dist/developer/upgrades/5.3-to-5.4.md +1 -1
  43. package/dist/developer/upgrades/5.4-to-5.5.md +1 -1
  44. package/dist/integrations/integrations.md +0 -7
  45. package/package.json +1 -1
  46. package/dist/integrations/sso-mfa-social-login/admin-dashboard.md +0 -57
  47. package/dist/integrations/sso-mfa-social-login/storefront.md +0 -56
@@ -20,7 +20,7 @@ Every store ships with one default channel named *Online Store*. You can add mor
20
20
  | `code` | URL-safe slug, stable identifier sent via the `X-Spree-Channel` header | `pos` |
21
21
  | `active` | When `false`, the channel stops accepting orders | `true` |
22
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` |
23
+ | `preferred_order_routing_strategy` | Optional per-channel override of the store's [Order Routing](shipments.md#order-routing) strategy | `Spree::OrderRouting::Strategy::Rules` |
24
24
 
25
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
26
 
@@ -37,10 +37,10 @@ The resolved channel is then available to controllers, models, and serializers t
37
37
 
38
38
  ### Selecting a channel from the Store SDK
39
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_…`).
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_…`). `setChannel` is a sticky setter that [mirrors `setLocale` / `setCurrency` / `setCountry`](../sdk/configuration.md).
41
41
 
42
42
 
43
- ```typescript SDK
43
+ ```typescript Store SDK
44
44
  // Client-level default
45
45
  const client = createClient({
46
46
  baseUrl: 'https://api.mystore.com',
@@ -55,6 +55,11 @@ client.setChannel('wholesale')
55
55
  const products = await client.products.list({}, { channel: 'pos' })
56
56
  ```
57
57
 
58
+ ```typescript Admin SDK
59
+ // admin filters by the channel's code via Ransack (q[channels_code_eq])
60
+ const { data: products } = await adminClient.products.list({ channels_code_eq: 'pos' })
61
+ ```
62
+
58
63
  ```bash cURL
59
64
  curl 'https://api.mystore.com/api/v3/store/products' \
60
65
  -H 'X-Spree-API-Key: pk_xxx' \
@@ -62,7 +67,7 @@ curl 'https://api.mystore.com/api/v3/store/products' \
62
67
  ```
63
68
 
64
69
 
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).
70
+ 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](../../api-reference/admin-api/querying.md) (`q[channel_id_eq]=ch_xxx` for orders, `q[channels_id_in][]=ch_xxx` for products).
66
71
 
67
72
  ### Product visibility
68
73
 
@@ -103,7 +108,7 @@ Three endpoints cover the publishing surface:
103
108
  | `POST /api/v3/admin/products/bulk_add_to_channels` | Publish many products across many channels in a single request |
104
109
 
105
110
 
106
- ```typescript SDK
111
+ ```typescript Admin SDK
107
112
  await adminClient.channels.addProducts('ch_xxx', {
108
113
  product_ids: ['prod_aaa', 'prod_bbb'],
109
114
  // Optional window — when omitted, existing schedules are preserved
@@ -129,7 +134,7 @@ curl -X POST 'https://api.mystore.com/api/v3/admin/channels/ch_xxx/add_products'
129
134
  For per-product updates, use `PATCH /api/v3/admin/products/:id` with a `product_publications` array:
130
135
 
131
136
 
132
- ```typescript SDK
137
+ ```typescript Admin SDK
133
138
  await adminClient.products.update('prod_xxx', {
134
139
  product_publications: [
135
140
  { channel_id: 'ch_online' },
@@ -165,3 +170,5 @@ The write contract is **full-set**: the array represents the complete desired st
165
170
  - [Markets](markets.md) — Different from channels: markets segment geography/currency, channels segment selling surfaces
166
171
  - [Products](products.md) — Product catalog and publication
167
172
  - [Order Routing](shipments.md#order-routing) — Channels can override the store's routing strategy
173
+ - [Store SDK: Products](../sdk/store/products.md) — Channel-scoped product listing and filtering
174
+ - [Admin SDK: Resources](../sdk/admin/resources.md) — How `adminClient.channels.addProducts` and other resource methods are structured
@@ -45,7 +45,7 @@ erDiagram
45
45
  ## Registration
46
46
 
47
47
 
48
- ```typescript SDK
48
+ ```typescript Store SDK
49
49
  const { token, user } = await client.customers.create({
50
50
  email: 'john@example.com',
51
51
  password: 'password123',
@@ -54,7 +54,7 @@ const { token, user } = await client.customers.create({
54
54
  last_name: 'Doe',
55
55
  })
56
56
  // token => JWT token for subsequent authenticated requests
57
- // user => { id: "usr_xxx", email: "john@example.com", first_name: "John", ... }
57
+ // user => { id: "cus_abc123", email: "john@example.com", first_name: "John", ... }
58
58
  ```
59
59
 
60
60
  ```bash cURL
@@ -71,10 +71,28 @@ curl -X POST 'https://api.mystore.com/api/v3/store/customers' \
71
71
  ```
72
72
 
73
73
 
74
+ > **NOTE:** The Store API flow above is **customer self-registration** — the customer sets a password and gets a JWT back. To [create a customer record from the back office](../sdk/admin/resources.md) (importers, CRM sync, phone orders), use the [Admin SDK](../sdk/admin/quickstart.md) instead. No password is set, so it's a managed record rather than a sign-up:
75
+ >
76
+ > ```typescript
77
+ import { createAdminClient } from '@spree/admin-sdk'
78
+
79
+ const client = createAdminClient({
80
+ baseUrl: 'https://store.example.com',
81
+ secretKey: 'sk_xxx',
82
+ })
83
+
84
+ const customer = await client.customers.create({
85
+ email: 'john@example.com',
86
+ first_name: 'John',
87
+ last_name: 'Doe',
88
+ tags: ['wholesale'],
89
+ })
90
+ ```
91
+
74
92
  ## Login
75
93
 
76
94
 
77
- ```typescript SDK
95
+ ```typescript Store SDK
78
96
  const { token, user } = await client.auth.login({
79
97
  email: 'john@example.com',
80
98
  password: 'password123',
@@ -93,22 +111,25 @@ curl -X POST 'https://api.mystore.com/api/v3/store/auth/login' \
93
111
  ```
94
112
 
95
113
 
96
- The response includes a JWT `token` and a `user` object. Pass the token in subsequent requests via the `Authorization: Bearer <token>` header.
114
+ The response includes a JWT `token` and a `user` object. Pass the token in subsequent requests via the [`Authorization: Bearer <token>`](../../api-reference/store-api/authentication.md) header.
97
115
 
98
116
  ## Token Refresh
99
117
 
100
- Refresh an expiring token to keep the session alive:
118
+ Refresh an expiring token to keep the session alive. For how `client.auth.login` and `client.auth.refresh` fit into configuring the SDK for guest vs customer (JWT) auth, see the [SDK authentication guide](../sdk/authentication.md):
101
119
 
102
120
 
103
- ```typescript SDK
104
- const { token } = await client.auth.refresh({
105
- token: existingToken,
121
+ ```typescript Store SDK
122
+ const { token, refresh_token } = await client.auth.refresh({
123
+ refresh_token: existingRefreshToken,
106
124
  })
125
+ // Persist the rotated refresh_token for the next refresh
107
126
  ```
108
127
 
109
128
  ```bash cURL
110
129
  curl -X POST 'https://api.mystore.com/api/v3/store/auth/refresh' \
111
- -H 'Authorization: Bearer <jwt_token>'
130
+ -H 'X-Spree-Api-Key: pk_xxx' \
131
+ -H 'Content-Type: application/json' \
132
+ -d '{ "refresh_token": "rt_xxx" }'
112
133
  ```
113
134
 
114
135
 
@@ -119,8 +140,8 @@ Password reset is a two-step flow. First, request a reset email. Then, use the t
119
140
  ### Step 1: Request Reset
120
141
 
121
142
 
122
- ```typescript SDK
123
- await client.customer.passwordResets.create({
143
+ ```typescript Store SDK
144
+ await client.passwordResets.create({
124
145
  email: 'john@example.com',
125
146
  redirect_url: 'https://myshop.com/reset-password',
126
147
  })
@@ -129,7 +150,7 @@ await client.customer.passwordResets.create({
129
150
  ```
130
151
 
131
152
  ```bash cURL
132
- curl -X POST 'https://api.mystore.com/api/v3/store/customer/password_resets' \
153
+ curl -X POST 'https://api.mystore.com/api/v3/store/password_resets' \
133
154
  -H 'X-Spree-Api-Key: pk_xxx' \
134
155
  -H 'Content-Type: application/json' \
135
156
  -d '{
@@ -139,15 +160,15 @@ curl -X POST 'https://api.mystore.com/api/v3/store/customer/password_resets' \
139
160
  ```
140
161
 
141
162
 
142
- The optional `redirect_url` parameter specifies where the password reset link in the email should point to. The token will be appended as a query parameter (e.g., `https://myshop.com/reset-password?token=...`). If the store has [Allowed Origins](allowed-origins.md) configured, the `redirect_url` must match one of them.
163
+ The optional `redirect_url` parameter specifies where the password reset link in the email should point to. The token will be appended as a query parameter (e.g., `https://myshop.com/reset-password?token=...`). If the store has Allowed Origins configured, the `redirect_url` must match one of them.
143
164
 
144
165
  This fires a `customer.password_reset_requested` event with the reset token in the payload. If you're using the `spree_emails` package, the email is sent automatically. Otherwise, subscribe to this event to send the reset email yourself (see [Events](events.md)).
145
166
 
146
167
  ### Step 2: Reset Password
147
168
 
148
169
 
149
- ```typescript SDK
150
- const { token, user } = await client.customer.passwordResets.update(
170
+ ```typescript Store SDK
171
+ const { token, user } = await client.passwordResets.update(
151
172
  'reset-token-from-email',
152
173
  {
153
174
  password: 'newsecurepassword',
@@ -158,7 +179,7 @@ const { token, user } = await client.customer.passwordResets.update(
158
179
  ```
159
180
 
160
181
  ```bash cURL
161
- curl -X PATCH 'https://api.mystore.com/api/v3/store/customer/password_resets/RESET_TOKEN' \
182
+ curl -X PATCH 'https://api.mystore.com/api/v3/store/password_resets/RESET_TOKEN' \
162
183
  -H 'X-Spree-Api-Key: pk_xxx' \
163
184
  -H 'Content-Type: application/json' \
164
185
  -d '{
@@ -178,7 +199,7 @@ Headless storefronts often need to collect newsletter signups before account cre
178
199
  ### Step 1: Subscribe
179
200
 
180
201
 
181
- ```typescript SDK
202
+ ```typescript Store SDK
182
203
  await client.newsletterSubscribers.create({
183
204
  email: 'guest@example.com',
184
205
  redirect_url: 'https://myshop.com/newsletter/confirm',
@@ -197,7 +218,7 @@ curl -X POST 'https://api.mystore.com/api/v3/store/newsletter_subscribers' \
197
218
  ```
198
219
 
199
220
 
200
- The optional `redirect_url` points at the storefront page that will receive the verification token. It is validated against the store's [Allowed Origins](allowed-origins.md) — URLs that do not match are silently dropped from the webhook payload (secure-by-default).
221
+ The optional `redirect_url` points at the storefront page that will receive the verification token. It is validated against the store's Allowed Origins — URLs that do not match are silently dropped from the webhook payload (secure-by-default).
201
222
 
202
223
  This fires a `newsletter_subscriber.subscription_requested` event whose payload includes `email`, `verification_token`, and the validated `redirect_url`. Subscribe to this event from your storefront's webhook handler to send the confirmation email — the link in the email should point to `<redirect_url>?token=<verification_token>`. The bundled `spree_emails` package also listens to this event and sends a default confirmation email if you're not running a headless storefront.
203
224
 
@@ -206,7 +227,7 @@ If the request is authenticated via a customer JWT and the JWT's email matches t
206
227
  ### Step 2: Verify
207
228
 
208
229
 
209
- ```typescript SDK
230
+ ```typescript Store SDK
210
231
  await client.newsletterSubscribers.verify({
211
232
  token: 'token-from-email',
212
233
  })
@@ -226,11 +247,11 @@ On success, the subscriber is marked verified. If the subscription is linked to
226
247
  ## Customer Profile
227
248
 
228
249
 
229
- ```typescript SDK
250
+ ```typescript Store SDK
230
251
  // Get current customer
231
252
  const customer = await client.customer.get()
232
253
  // {
233
- // id: "usr_xxx",
254
+ // id: "cus_xxx",
234
255
  // email: "john@example.com",
235
256
  // first_name: "John",
236
257
  // last_name: "Doe",
@@ -248,11 +269,13 @@ const updated = await client.customer.update({
248
269
 
249
270
  ```bash cURL
250
271
  # Get current customer
251
- curl 'https://api.mystore.com/api/v3/store/customer' \
272
+ curl 'https://api.mystore.com/api/v3/store/customers/me' \
273
+ -H 'X-Spree-API-Key: pk_xxx' \
252
274
  -H 'Authorization: Bearer <jwt_token>'
253
275
 
254
276
  # Update profile
255
- curl -X PATCH 'https://api.mystore.com/api/v3/store/customer' \
277
+ curl -X PATCH 'https://api.mystore.com/api/v3/store/customers/me' \
278
+ -H 'X-Spree-API-Key: pk_xxx' \
256
279
  -H 'Authorization: Bearer <jwt_token>' \
257
280
  -H 'Content-Type: application/json' \
258
281
  -d '{ "first_name": "Jonathan", "accepts_email_marketing": true }'
@@ -279,6 +302,7 @@ Customers don't need to register to purchase. Guest checkout uses an order token
279
302
 
280
303
  ## Related Documentation
281
304
 
305
+ - [Account (Store SDK)](../sdk/store/account.md) — Task-oriented walkthrough of registration, login, profile, addresses, and order history
282
306
  - [Addresses](addresses.md) — Customer address management
283
307
  - [Orders](orders.md) — Order history and checkout
284
308
  - [Authentication](../customization/authentication.md) — Custom authentication setup
@@ -163,7 +163,7 @@ def handle(event)
163
163
  event.name # => "order.completed"
164
164
  event.store_id # => 1 (ID of the store where the event originated)
165
165
  event.payload # => { "id" => 1, "number" => "R123456", ... }
166
- event.metadata # => { "spree_version" => "5.1.0" }
166
+ event.metadata # => { "spree_version" => "<spree_version>" }
167
167
  event.created_at # => Time when event was published
168
168
 
169
169
  # Helper methods
@@ -243,13 +243,15 @@ Models with lifecycle events enabled include: `Order`, `Payment`, `Price`, `Ship
243
243
  | `price.updated` | Price was updated |
244
244
  | `price.deleted` | Price was deleted |
245
245
 
246
- ### Customer Events
246
+ ### User Events
247
247
 
248
248
  | Event | Description |
249
249
  |-------|-------------|
250
- | `customer.created` | Customer was created |
251
- | `customer.updated` | Customer was updated |
252
- | `customer.deleted` | Customer was deleted |
250
+ | `user.created` | User was created |
251
+ | `user.updated` | User was updated |
252
+ | `user.deleted` | User was deleted |
253
+
254
+ When `Spree.admin_user_class` differs from `Spree.user_class`, admin users publish the equivalent `admin.*` events (see the Admin Events table below).
253
255
 
254
256
  ### Admin Events
255
257
 
@@ -263,8 +265,8 @@ Models with lifecycle events enabled include: `Order`, `Payment`, `Price`, `Ship
263
265
 
264
266
  | Event | Description |
265
267
  |-------|-------------|
266
- | `product.activate` | Product status changed to active |
267
- | `product.archive` | Product status changed to archived |
268
+ | `product.activated` | Product status changed to active |
269
+ | `product.archived` | Product status changed to archived |
268
270
  | `product.out_of_stock` | Product has no stock left for any variant |
269
271
  | `product.back_in_stock` | Product was out of stock and now has stock again |
270
272
 
@@ -298,7 +300,7 @@ Spree::Events.publish(
298
300
 
299
301
  ## Event Serializers
300
302
 
301
- Event payloads are generated using the same [Store API V3 serializers](../../api-reference/introduction.md) used by the REST API. This means webhook payloads and API responses share the same schema, making it easy to reuse types in your integrations.
303
+ Event payloads are generated using the same [Store API V3 serializers](../../api-reference/store-api/introduction.md) used by the REST API. This means webhook payloads and API responses share the same schema, making it easy to reuse types in your integrations.
302
304
 
303
305
  ### How Serializers Work
304
306
 
@@ -344,7 +346,7 @@ This means event payloads contain the same top-level attributes and unconditiona
344
346
 
345
347
  ### Overriding Event Serializers
346
348
 
347
- To customize the payload for existing events, create a custom V3 serializer and configure it via dependencies:
349
+ To customize the payload for existing events, create a custom V3 serializer and configure it via [dependencies](../customization/dependencies.md):
348
350
 
349
351
  ```ruby app/serializers/my_app/order_serializer.rb
350
352
  module MyApp
@@ -488,7 +490,7 @@ end
488
490
 
489
491
  ### Testing Event Publishing
490
492
 
491
- Use the `emit_webhook_event` matcher (if available) or stub the events:
493
+ Stub `Spree::Events.publish` to assert an event is published:
492
494
 
493
495
  ```ruby
494
496
  it 'publishes order.completed event' do
@@ -501,6 +503,8 @@ it 'publishes order.completed event' do
501
503
  end
502
504
  ```
503
505
 
506
+ The `lifecycle events` shared examples in `spree/core/lib/spree/testing_support/lifecycle_events.rb` cover the standard `created`/`updated`/`deleted` lifecycle events.
507
+
504
508
  ## Best Practices
505
509
 
506
510
 
@@ -526,7 +530,7 @@ class InventoryAlertSubscriber < Spree::Subscriber
526
530
  def handle(event)
527
531
  stock_item = find_stock_item(event)
528
532
  return unless stock_item
529
- return unless stock_dropped_below_threshold?(event, stock_item)
533
+ return unless low_stock?(event)
530
534
 
531
535
  send_low_stock_alert(stock_item)
532
536
  end
@@ -537,11 +541,8 @@ class InventoryAlertSubscriber < Spree::Subscriber
537
541
  Spree::StockItem.find_by_prefix_id(event.payload['id'])
538
542
  end
539
543
 
540
- def stock_dropped_below_threshold?(event, stock_item)
541
- previous_count = event.payload['count_on_hand_before_last_save']
542
- current_count = stock_item.count_on_hand
543
-
544
- previous_count >= LOW_STOCK_THRESHOLD && current_count < LOW_STOCK_THRESHOLD
544
+ def low_stock?(event)
545
+ event.payload['count_on_hand'].to_i < LOW_STOCK_THRESHOLD
545
546
  end
546
547
 
547
548
  def send_low_stock_alert(stock_item)
@@ -554,6 +555,8 @@ class InventoryAlertSubscriber < Spree::Subscriber
554
555
  end
555
556
  ```
556
557
 
558
+ > **NOTE:** A lifecycle event payload carries only the resource's current serialized state, not its previous values. If you need a true "just dropped below the threshold" check that compares the count before and after the change, record or cache the prior count separately rather than reading it from the event payload.
559
+
557
560
  ## Custom Event Adapters
558
561
 
559
562
  Spree's event system uses an adapter pattern, making it possible to swap the underlying event infrastructure. By default, Spree uses [`ActiveSupport::Notifications`](https://api.rubyonrails.org/classes/ActiveSupport/Notifications.html), but you can create custom adapters for other backends like Kafka, RabbitMQ, or Redis Pub/Sub.
@@ -626,14 +629,16 @@ The `Spree::Events::Adapters::Base` class defines the required interface:
626
629
 
627
630
  The base class also provides helper methods:
628
631
  - `build_event(name, payload, metadata)` - Creates a `Spree::Event` instance
629
- - `subscriptions_for(event_name)` - Finds matching subscriptions from the registry
632
+ - `invoke_subscribers(event)` - Finds and invokes matching subscribers (internally calling `registry.subscriptions_for`)
630
633
  - `registry` - Access to the `Spree::Events::Registry` instance
631
634
 
632
635
  > **INFO:** See `Spree::Events::Adapters::ActiveSupportNotifications` for a complete reference implementation.
633
636
 
634
637
  ## Related Documentation
635
638
 
639
+ - [Events Tutorial](../tutorial/events.md) - Hands-on, step-by-step walkthrough of building an event subscriber
636
640
  - [Webhooks](webhooks.md) - HTTP callbacks for external integrations
641
+ - [Webhooks & Events Reference](../../api-reference/webhooks-events.md) - Catalog of event and webhook payloads
637
642
  - [Customization Quickstart](../customization/quickstart.md) - Overview of all customization options
638
643
  - [Decorators](../customization/decorators.md) - When to use decorators vs events
639
644
  - [Checkout Flow](../customization/checkout.md) - Using events in checkout customization
@@ -42,7 +42,7 @@ erDiagram
42
42
  Export {
43
43
  string number
44
44
  string type
45
- string format
45
+ integer format
46
46
  jsonb search_params
47
47
  }
48
48
 
@@ -80,10 +80,12 @@ Exports generate CSV files from filtered database records.
80
80
  | Type | Description | Multi-line |
81
81
  |------|-------------|------------|
82
82
  | `Spree::Exports::Products` | Products with all variants | Yes |
83
+ | `Spree::Exports::ProductTranslations` | Product translations for non-default store locales | Yes |
83
84
  | `Spree::Exports::Orders` | Orders with line items | Yes |
84
85
  | `Spree::Exports::Customers` | Customer accounts | No |
85
86
  | `Spree::Exports::GiftCards` | Gift cards | No |
86
87
  | `Spree::Exports::NewsletterSubscribers` | Newsletter subscribers | No |
88
+ | `Spree::Exports::CouponCodes` | Promotion coupon codes | No |
87
89
 
88
90
  ### Export Model
89
91
 
@@ -219,7 +221,7 @@ end
219
221
 
220
222
  ### Export Filtering
221
223
 
222
- Exports support Ransack filtering via `search_params`:
224
+ Exports support [Ransack filtering via `search_params`](../../api-reference/admin-api/querying.md):
223
225
 
224
226
  ```ruby
225
227
  # In admin, users can filter before exporting
@@ -242,6 +244,8 @@ Imports process CSV files to create or update database records.
242
244
  | Type | Description |
243
245
  |------|-------------|
244
246
  | `Spree::Imports::Products` | Products and variants |
247
+ | `Spree::Imports::ProductTranslations` | Product translations (matched by slug) |
248
+ | `Spree::Imports::Customers` | Customers |
245
249
 
246
250
  ### Import Workflow
247
251
 
@@ -538,7 +542,7 @@ The products import handles variants intelligently:
538
542
 
539
543
  ```csv
540
544
  slug,sku,name,price,option1_name,option1_value,option2_name,option2_value
541
- my-tshirt,TSHIRT-001,My T-Shirt,29.99,,,
545
+ my-tshirt,TSHIRT-001,My T-Shirt,29.99,,,,
542
546
  my-tshirt,TSHIRT-S-RED,My T-Shirt,29.99,Size,Small,Color,Red
543
547
  my-tshirt,TSHIRT-M-RED,My T-Shirt,29.99,Size,Medium,Color,Red
544
548
  my-tshirt,TSHIRT-L-RED,My T-Shirt,29.99,Size,Large,Color,Red
@@ -560,16 +564,16 @@ Both imports and exports support metafields dynamically:
560
564
 
561
565
  Parses CSV and creates ImportRow records:
562
566
 
567
+ All import jobs inherit from `Spree::Imports::BaseJob`, which sets `queue_as Spree.queues.imports` once and is shared across the pipeline.
568
+
563
569
  ```ruby
564
570
  module Spree
565
571
  module Imports
566
- class CreateRowsJob < Spree::BaseJob
567
- queue_as Spree.queues.imports
568
-
572
+ class CreateRowsJob < Spree::Imports::BaseJob
569
573
  def perform(import_id)
570
574
  import = Spree::Import.find(import_id)
571
575
  # Stream CSV, batch insert rows
572
- # Then enqueue ProcessRowsJob
576
+ # Then enqueue ProcessRowsJob via import.process_rows_async
573
577
  end
574
578
  end
575
579
  end
@@ -578,20 +582,64 @@ end
578
582
 
579
583
  ### ProcessRowsJob
580
584
 
581
- Processes pending rows through row processors:
585
+ Fans the pending rows out into groups, each processed by a separate `ProcessGroupJob`:
582
586
 
583
587
  ```ruby
584
588
  module Spree
585
589
  module Imports
586
- class ProcessRowsJob < Spree::BaseJob
587
- queue_as Spree.queues.imports
590
+ class ProcessRowsJob < Spree::Imports::BaseJob
591
+ BATCH_SIZE = 100
588
592
 
589
593
  def perform(import_id)
590
594
  import = Spree::Import.find(import_id)
591
- import.rows.pending_and_failed.find_each do |row|
592
- row.process!
595
+ dispatch_groups(import)
596
+ end
597
+
598
+ private
599
+
600
+ def dispatch_groups(import)
601
+ # When the import defines a group_column (e.g. ProductTranslations groups
602
+ # by slug) and that field is mapped, group rows by the column value.
603
+ # Otherwise, fall back to slicing pending_and_failed rows into batches of
604
+ # BATCH_SIZE. Either way: pluck the row IDs, set processing_groups_count /
605
+ # completed_groups_count up front so workers can't complete prematurely,
606
+ # then enqueue one ProcessGroupJob per group.
607
+ import.rows.pending_and_failed.in_batches(of: BATCH_SIZE) do |batch|
608
+ ProcessGroupJob.perform_later(import.id, batch.ids)
593
609
  end
594
- import.complete!
610
+ end
611
+ end
612
+ end
613
+ end
614
+ ```
615
+
616
+ ### ProcessGroupJob
617
+
618
+ Processes one group of rows and reports completion:
619
+
620
+ ```ruby
621
+ module Spree
622
+ module Imports
623
+ class ProcessGroupJob < Spree::Imports::BaseJob
624
+ def perform(import_id, row_ids)
625
+ import = Spree::Import.find(import_id)
626
+ rows = import.rows.where(id: row_ids).pending_and_failed
627
+
628
+ # Large imports process with events disabled and use bulk_process!;
629
+ # otherwise each row is processed individually via row.process!.
630
+ rows.each { |row| row.process! }
631
+
632
+ check_import_completion(import)
633
+ end
634
+
635
+ private
636
+
637
+ # Increments completed_groups_count, then completes the import only once the
638
+ # last group has finished and no rows are still in flight.
639
+ def check_import_completion(import)
640
+ # completed_groups_count += 1
641
+ import.complete! if import.completed_groups_count >= import.processing_groups_count &&
642
+ import.rows.in_flight.none?
595
643
  end
596
644
  end
597
645
  end
@@ -609,7 +657,7 @@ module Spree
609
657
  queue_as Spree.queues.exports
610
658
 
611
659
  def perform(export_id)
612
- export = Spree::Export.find(export_id)
660
+ export = Spree::Export.find_by_prefix_id!(export_id)
613
661
  export.generate
614
662
  end
615
663
  end
@@ -621,6 +669,8 @@ end
621
669
 
622
670
  ## Events
623
671
 
672
+ Import and export lifecycle events such as [`import.completed` and `export.created`](../../api-reference/webhooks-events.md) can be delivered as webhooks to react when an asynchronous import or export finishes.
673
+
624
674
  ### Import Events
625
675
 
626
676
  | Event | Trigger |
@@ -696,3 +746,8 @@ can :manage, Spree::Export
696
746
  ```
697
747
 
698
748
  Records are filtered by `current_ability` ensuring users only export data they have access to.
749
+
750
+ ## Related Documentation
751
+
752
+ - [Admin imports/exports endpoints](../../api-reference/admin-api/endpoints.md) — REST routes and required scopes to trigger imports and exports programmatically.
753
+ - [Admin SDK resources](../sdk/admin/resources.md) — typed TypeScript client for driving these back-office operations.
@@ -115,6 +115,38 @@ Stock Locations can be easily used for tracking warehouses and other physical lo
115
115
 
116
116
  You can easily use them with your Point of Sale (POS) system to track inventory at different locations.
117
117
 
118
+ Create and manage stock locations via the [Admin API](../../api-reference/admin-api/introduction.md):
119
+
120
+
121
+ ```typescript Admin SDK
122
+ import { createAdminClient } from '@spree/admin-sdk'
123
+
124
+ const client = createAdminClient({
125
+ baseUrl: 'https://store.example.com',
126
+ secretKey: 'sk_xxx',
127
+ })
128
+
129
+ const location = await client.stockLocations.create({
130
+ name: 'Warehouse 1',
131
+ admin_name: 'WH1 Domestic',
132
+ default: true,
133
+ country_iso: 'US',
134
+ propagate_all_variants: true,
135
+ })
136
+
137
+ await client.stockLocations.update('sloc_xxx', { active: false })
138
+ ```
139
+
140
+ ```bash CLI
141
+ spree api post /stock_locations -d '{
142
+ "name": "Warehouse 1",
143
+ "default": true,
144
+ "country_iso": "US",
145
+ "propagate_all_variants": true
146
+ }'
147
+ ```
148
+
149
+
118
150
  ### Stock Items
119
151
 
120
152
  Stock Items represent the inventory at a stock location for a specific variant. Stock item count on hand can be increased or decreased by creating stock movements.
@@ -126,6 +158,25 @@ Stock Items represent the inventory at a stock location for a specific variant.
126
158
  | `count_on_hand` | The number of items available on hand. | `150` |
127
159
  | `backorderable` | Indicates whether the stock item can be backordered. | `true` |
128
160
 
161
+ Stock items are created automatically — for all variants when a location has `propagate_all_variants`, or via a variant's `stock_items` on create. To adjust quantity or backorderable status, **update** the existing stock item via the Admin API. The example below uses a Ransack predicate (`stock_location_id_eq`) to [list a location's stock items](../../api-reference/admin-api/querying.md) before updating one:
162
+
163
+
164
+ ```typescript Admin SDK
165
+ // List a location's stock items, then adjust one
166
+ const { data: items } = await client.stockItems.list({ stock_location_id_eq: 'sloc_xxx' })
167
+
168
+ await client.stockItems.update('si_xxx', {
169
+ count_on_hand: 150,
170
+ backorderable: true,
171
+ })
172
+ ```
173
+
174
+ ```bash CLI
175
+ spree api get /stock_items -q stock_location_id_eq=sloc_xxx
176
+ spree api patch /stock_items/si_xxx -d '{"count_on_hand": 150, "backorderable": true}'
177
+ ```
178
+
179
+
129
180
  ### Stock Transfers
130
181
 
131
182
  Stock transfers allow you to move inventory in bulk from one stock location to another stock location. This is handy when you want to integrate with a POS system or other inventory management system. Or you can just rely on Spree being the source of truth for your inventory.
@@ -141,6 +192,30 @@ Here's the list of attributes for the Stock Transfer model:
141
192
  | `source_location_id` | The ID of the stock location where the stock is transferred from. | `2` |
142
193
  | `destination_location_id`| The ID of the stock location where the stock is transferred to. | `3` |
143
194
 
195
+ Create a transfer via the [Admin API](../../api-reference/admin-api/introduction.md), listing the variants and quantities to move. Omit `source_location_id` to record an incoming receipt from a vendor:
196
+
197
+
198
+ ```typescript Admin SDK
199
+ const transfer = await client.stockTransfers.create({
200
+ source_location_id: 'sloc_warehouse',
201
+ destination_location_id: 'sloc_store',
202
+ reference: 'Transfer for Event',
203
+ variants: [
204
+ { variant_id: 'variant_xxx', quantity: 20 },
205
+ { variant_id: 'variant_yyy', quantity: 5 },
206
+ ],
207
+ })
208
+ ```
209
+
210
+ ```bash CLI
211
+ spree api post /stock_transfers -d '{
212
+ "source_location_id": "sloc_warehouse",
213
+ "destination_location_id": "sloc_store",
214
+ "variants": [{ "variant_id": "variant_xxx", "quantity": 20 }]
215
+ }'
216
+ ```
217
+
218
+
144
219
  Stock transfers are crucial for managing inventory across multiple locations, ensuring that stock levels are accurate and up-to-date.
145
220
 
146
221
  Each Stock Transfer will hold a list of Stock Movements.
@@ -191,7 +266,7 @@ Reservations attach to each line item; when a line item or order is removed, the
191
266
  |---|---|---|
192
267
  | `Spree::Config[:stock_reservations_enabled]` | `true` | Global kill switch. When `false`, reservations are not created and availability ignores them — behavior matches pre-5.5. |
193
268
  | `Spree::Config[:default_stock_reservation_ttl_minutes]` | `10` | Fallback hold duration when a Store doesn't override. |
194
- | `store.preferred_stock_reservation_ttl_minutes` | inherits global | Per-Store override. |
269
+ | `store.preferred_stock_reservation_ttl_minutes` | `10` | Per-Store override. Falls back to the global default only when explicitly unset/blank. |
195
270
 
196
271
  TTL is a Store-level setting — it's a checkout-experience policy, not a warehouse property. A multi-location cart never has to merge conflicting TTLs from different warehouses.
197
272
 
@@ -230,6 +305,7 @@ Inventory Units states are:
230
305
  * `on_hand` - the inventory unit is on hand
231
306
  * `backordered` - the inventory unit is backordered
232
307
  * `shipped` - the inventory unit is shipped
308
+ * `returned` - the inventory unit has been returned
233
309
 
234
310
  > **NOTE:** As we noted before, when you add new Stock Items to a Variant (eg. via Admin Panel or Admin API), the first Inventory Units to fulfill are the backordered ones.
235
311
 
@@ -245,3 +321,5 @@ If you don't need to track inventory, you can disable it:
245
321
  - [Products](products.md) - Product and variant management
246
322
  - [Shipments](shipments.md) - How inventory relates to shipments
247
323
  - [Orders](orders.md) - How inventory is allocated to orders
324
+ - [Admin SDK resources](../sdk/admin/resources.md) - `stockLocations`, `stockItems`, and `stockTransfers` methods used in the examples above
325
+ - [Admin API authentication](../../api-reference/admin-api/authentication.md) - How to obtain and scope the secret key (`sk_xxx`) used by these calls