@spree/docs 0.1.93 → 0.1.94
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-reference/admin-api/authentication.md +8 -7
- package/dist/api-reference/admin-api/endpoints.md +348 -0
- package/dist/api-reference/admin-api/errors.md +2 -0
- package/dist/api-reference/admin-api/introduction.md +11 -0
- package/dist/api-reference/store.yaml +243 -232
- package/dist/developer/agentic/overview.md +1 -1
- package/dist/developer/cli/admin-api.md +146 -0
- package/dist/developer/cli/quickstart.md +40 -5
- package/dist/developer/core-concepts/addresses.md +32 -16
- package/dist/developer/core-concepts/adjustments.md +11 -5
- package/dist/developer/core-concepts/architecture.md +8 -8
- package/dist/developer/core-concepts/calculators.md +31 -51
- package/dist/developer/core-concepts/channels.md +13 -6
- package/dist/developer/core-concepts/customers.md +47 -23
- package/dist/developer/core-concepts/events.md +22 -17
- package/dist/developer/core-concepts/imports-exports.md +69 -14
- package/dist/developer/core-concepts/inventory.md +79 -1
- package/dist/developer/core-concepts/markets.md +64 -20
- package/dist/developer/core-concepts/media.md +43 -6
- package/dist/developer/core-concepts/metafields.md +76 -13
- package/dist/developer/core-concepts/orders.md +95 -17
- package/dist/developer/core-concepts/payments.md +14 -13
- package/dist/developer/core-concepts/pricing.md +95 -9
- package/dist/developer/core-concepts/products.md +192 -26
- package/dist/developer/core-concepts/promotions.md +61 -4
- package/dist/developer/core-concepts/reports.md +4 -2
- package/dist/developer/core-concepts/search-filtering.md +82 -32
- package/dist/developer/core-concepts/shipments.md +16 -13
- package/dist/developer/core-concepts/slugs.md +20 -11
- package/dist/developer/core-concepts/staff-roles.md +51 -1
- package/dist/developer/core-concepts/store-credits-gift-cards.md +90 -9
- package/dist/developer/core-concepts/stores.md +16 -14
- package/dist/developer/core-concepts/taxes.md +28 -0
- package/dist/developer/core-concepts/translations.md +16 -7
- package/dist/developer/core-concepts/users.md +13 -9
- package/dist/developer/core-concepts/webhooks.md +95 -64
- package/dist/developer/how-to/custom-api-authentication.md +103 -23
- package/dist/developer/multi-store/quickstart.md +1 -1
- package/dist/developer/sdk/admin/authentication.md +1 -1
- package/dist/developer/sdk/admin/resources.md +2 -0
- package/dist/developer/upgrades/5.3-to-5.4.md +1 -1
- package/dist/developer/upgrades/5.4-to-5.5.md +1 -1
- package/dist/integrations/integrations.md +0 -7
- package/package.json +1 -1
- package/dist/integrations/sso-mfa-social-login/admin-dashboard.md +0 -57
- 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: "
|
|
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
|
-
|
|
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 '
|
|
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.
|
|
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/
|
|
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
|
|
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.
|
|
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/
|
|
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
|
|
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: "
|
|
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/
|
|
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/
|
|
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" => "
|
|
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
|
-
###
|
|
246
|
+
### User Events
|
|
247
247
|
|
|
248
248
|
| Event | Description |
|
|
249
249
|
|-------|-------------|
|
|
250
|
-
| `
|
|
251
|
-
| `
|
|
252
|
-
| `
|
|
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.
|
|
267
|
-
| `product.
|
|
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
|
-
|
|
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
|
|
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
|
|
541
|
-
|
|
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
|
-
- `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
592
|
-
|
|
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
|
-
|
|
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.
|
|
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` |
|
|
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
|