@spree/docs 0.1.230 → 0.1.232

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 (46) hide show
  1. package/dist/api-reference/store.yaml +28 -0
  2. package/dist/developer/agentic/agent-skills.md +1 -1
  3. package/dist/developer/cli/quickstart.md +2 -2
  4. package/dist/developer/core-concepts/addresses.md +13 -0
  5. package/dist/developer/core-concepts/calculators.md +6 -1
  6. package/dist/developer/core-concepts/carts.md +19 -1
  7. package/dist/developer/core-concepts/catalogs.md +51 -2
  8. package/dist/developer/core-concepts/channels.md +20 -1
  9. package/dist/developer/core-concepts/commissions.md +70 -1
  10. package/dist/developer/core-concepts/companies.md +74 -0
  11. package/dist/developer/core-concepts/customers.md +17 -5
  12. package/dist/developer/core-concepts/data-privacy.md +20 -7
  13. package/dist/developer/core-concepts/delivery-setup.md +57 -0
  14. package/dist/developer/core-concepts/discounts.md +7 -0
  15. package/dist/developer/core-concepts/events.md +13 -7
  16. package/dist/developer/core-concepts/fees.md +20 -0
  17. package/dist/developer/core-concepts/fulfillments.md +3 -3
  18. package/dist/developer/core-concepts/imports-exports.md +21 -5
  19. package/dist/developer/core-concepts/inventory.md +73 -1
  20. package/dist/developer/core-concepts/markets.md +5 -6
  21. package/dist/developer/core-concepts/media.md +66 -1
  22. package/dist/developer/core-concepts/metafields.md +1 -1
  23. package/dist/developer/core-concepts/order-totals.md +18 -1
  24. package/dist/developer/core-concepts/orders.md +2 -1
  25. package/dist/developer/core-concepts/payments.md +100 -20
  26. package/dist/developer/core-concepts/pricing.md +13 -8
  27. package/dist/developer/core-concepts/products.md +52 -1
  28. package/dist/developer/core-concepts/reporting.md +5 -5
  29. package/dist/developer/core-concepts/returns-exchanges-claims.md +41 -2
  30. package/dist/developer/core-concepts/search-filtering.md +1 -2
  31. package/dist/developer/core-concepts/sellers.md +109 -2
  32. package/dist/developer/core-concepts/slugs.md +1 -1
  33. package/dist/developer/core-concepts/staff-roles.md +23 -3
  34. package/dist/developer/core-concepts/store-credits-gift-cards.md +60 -23
  35. package/dist/developer/core-concepts/stores.md +21 -0
  36. package/dist/developer/core-concepts/taxes.md +43 -0
  37. package/dist/developer/core-concepts/translations.md +9 -0
  38. package/dist/developer/core-concepts/webhooks.md +45 -3
  39. package/dist/developer/create-spree-app/quickstart.md +10 -10
  40. package/dist/developer/dashboard/deployment.md +4 -4
  41. package/dist/developer/dashboard/recipes/attribute-end-to-end.md +1 -1
  42. package/dist/developer/deployment/docker.md +2 -2
  43. package/dist/developer/how-to/custom-promotion.md +33 -13
  44. package/dist/developer/multi-tenant/quickstart.md +6 -6
  45. package/dist/developer/storefront/nextjs/testing.md +2 -2
  46. package/package.json +1 -1
@@ -163,6 +163,7 @@ how postage records work in general.
163
163
 
164
164
  What to do about a claim is decided when you resolve it, not when the customer opens it — merchants usually decide once they've seen the photos.
165
165
 
166
+
166
167
  ```typescript Admin SDK
167
168
  await adminClient.orders.claims.approve('or_xxx', 'clm_xxx')
168
169
 
@@ -173,32 +174,70 @@ await adminClient.orders.claims.resolve('or_xxx', 'clm_xxx', {
173
174
  })
174
175
  ```
175
176
 
177
+ ```bash cURL
178
+ curl -X PATCH 'https://api.mystore.com/api/v3/admin/orders/or_xxx/claims/clm_xxx/approve' \
179
+ -H 'X-Spree-API-Key: sk_xxx'
180
+
181
+ curl -X PATCH 'https://api.mystore.com/api/v3/admin/orders/or_xxx/claims/clm_xxx/resolve' \
182
+ -H 'X-Spree-API-Key: sk_xxx' \
183
+ -H 'Content-Type: application/json' \
184
+ -d '{
185
+ "resolution": "refund_and_replacement",
186
+ "refund_method": "store_credit",
187
+ "replacement_line_item_ids": ["li_xxx"]
188
+ }'
189
+ ```
190
+
191
+
176
192
  A replacement creates a new [fulfillment](fulfillments.md) on the original order, so the customer doesn't have to place a second one.
177
193
 
178
194
  ## Exchanges
179
195
 
180
196
  An exchange works like a return, but ends by sending different items instead of refunding:
181
197
 
198
+
182
199
  ```typescript Admin SDK
183
200
  await adminClient.orders.exchanges.approve('or_xxx', 'exch_xxx')
184
201
  await adminClient.orders.exchanges.receive('or_xxx', 'exch_xxx')
185
202
  await adminClient.orders.exchanges.fulfill('or_xxx', 'exch_xxx')
186
203
  ```
187
204
 
205
+ ```bash cURL
206
+ curl -X PATCH 'https://api.mystore.com/api/v3/admin/orders/or_xxx/exchanges/exch_xxx/approve' \
207
+ -H 'X-Spree-API-Key: sk_xxx'
208
+
209
+ curl -X PATCH 'https://api.mystore.com/api/v3/admin/orders/or_xxx/exchanges/exch_xxx/receive' \
210
+ -H 'X-Spree-API-Key: sk_xxx'
211
+
212
+ curl -X PATCH 'https://api.mystore.com/api/v3/admin/orders/or_xxx/exchanges/exch_xxx/fulfill' \
213
+ -H 'X-Spree-API-Key: sk_xxx'
214
+ ```
215
+
216
+
188
217
  ## Reporting
189
218
 
190
219
  Because each one is its own record, you can query them directly:
191
220
 
221
+
192
222
  ```typescript Admin SDK
193
223
  // Everything awaiting action
194
- const pending = await adminClient.returns.list({ filter: { status_eq: 'approved' } })
224
+ const pending = await adminClient.returns.list({ status_eq: 'approved' })
195
225
 
196
226
  // Claims opened this month
197
227
  const claims = await adminClient.claims.list({
198
- filter: { created_at_gt: '2026-08-01' },
228
+ created_at_gt: '2026-08-01',
199
229
  })
200
230
  ```
201
231
 
232
+ ```bash cURL
233
+ curl 'https://api.mystore.com/api/v3/admin/returns?q[status_eq]=approved' \
234
+ -H 'X-Spree-API-Key: sk_xxx'
235
+
236
+ curl 'https://api.mystore.com/api/v3/admin/claims?q[created_at_gt]=2026-08-01' \
237
+ -H 'X-Spree-API-Key: sk_xxx'
238
+ ```
239
+
240
+
202
241
  ## Return policy
203
242
 
204
243
  Spree ships no built-in return window. Whether a customer may open a return is store policy, and it's checked when the return is created — so you can hold customers to a 30-day window while letting staff make an exception for a good customer, and vary the rule by market where local law requires it.
@@ -278,8 +278,7 @@ See [Querying](../../api-reference/store-api/querying.md) for the full list of f
278
278
 
279
279
  Spree uses a pluggable search provider architecture. The default provider uses SQL (ILIKE + Ransack). For production catalogs with 1,000+ products, we recommend switching to [Meilisearch](../../integrations/search/meilisearch.md) for typo tolerance, relevance ranking, and faster faceted search.
280
280
 
281
- ```ruby
282
- # config/initializers/spree.rb
281
+ ```ruby server/config/initializers/spree.rb
283
282
  Spree.search_provider = 'Spree::SearchProvider::Meilisearch'
284
283
  ```
285
284
 
@@ -52,12 +52,62 @@ stateDiagram-v2
52
52
 
53
53
  Only an **approved** seller can sell, and even then not while they're on holiday — sellers can pause their own listings without the marketplace suspending them.
54
54
 
55
+ ## What a seller owns
56
+
57
+ A seller is a tenant inside the marketplace: their own catalogue, their own
58
+ stock, their own shipping setup and their own money trail. Delivery profiles and
59
+ zones stay with the operator — a seller picks from them rather than defining
60
+ them.
61
+
62
+ ```mermaid
63
+ erDiagram
64
+ Store ||--o{ Seller : "hosts"
65
+ Seller ||--o{ Product : "lists"
66
+ Seller ||--o{ StockLocation : "ships from"
67
+ Seller ||--o{ DeliveryMethod : "own rates"
68
+ Seller ||--o{ PackageType : "own packaging"
69
+ Seller ||--o{ Order : "fulfils"
70
+ Seller ||--o{ CommissionLine : "charged"
71
+ Seller ||--o{ SellerTransfer : "earns"
72
+ Seller ||--o{ SellerPayout : "paid out"
73
+ Seller ||--o{ SellerRequirementSubmission : "onboarding"
74
+ Seller ||--o{ Policy : "publishes"
75
+
76
+ Seller {
77
+ string name
78
+ string slug
79
+ string status
80
+ string contact_email
81
+ }
82
+ ```
83
+
84
+ A seller's delivery methods are their own — internal rates and manual
85
+ fulfillment, since carrier accounts stay with the operator. Their package types
86
+ sit alongside the marketplace's shared ones, so they can pack into the
87
+ operator's standard cartons or record their own.
88
+
89
+
55
90
  ```typescript Admin SDK
56
91
  await adminClient.sellers.invite('sel_xxx')
57
92
  await adminClient.sellers.approve('sel_xxx')
58
93
  await adminClient.sellers.suspend('sel_xxx', { reason: 'Unresolved delivery complaints' })
59
94
  ```
60
95
 
96
+ ```bash cURL
97
+ # Invite, approve, suspend
98
+ curl -X POST 'https://api.mystore.com/api/v3/admin/sellers/sel_xxx/invite' \
99
+ -H 'X-Spree-API-Key: sk_xxx'
100
+
101
+ curl -X POST 'https://api.mystore.com/api/v3/admin/sellers/sel_xxx/approve' \
102
+ -H 'X-Spree-API-Key: sk_xxx'
103
+
104
+ curl -X POST 'https://api.mystore.com/api/v3/admin/sellers/sel_xxx/suspend' \
105
+ -H 'X-Spree-API-Key: sk_xxx' \
106
+ -H 'Content-Type: application/json' \
107
+ -d '{ "reason": "Unresolved delivery complaints" }'
108
+ ```
109
+
110
+
61
111
  Status is never set by writing to the field — each move is its own action, so approving a seller can run the checks that belong to approving.
62
112
 
63
113
  ## Onboarding requirements
@@ -66,17 +116,35 @@ What a vendor must do before selling differs by marketplace. A hardware marketpl
66
116
 
67
117
  So the checklist is **configured, not hardcoded**. Each store defines its own:
68
118
 
119
+
69
120
  ```typescript Admin SDK
70
121
  const { data: types } = await adminClient.sellerRequirements.types()
71
122
 
72
123
  await adminClient.sellerRequirements.create({
73
- type: 'Spree::SellerRequirements::MinimumProducts',
124
+ type: 'minimum_products',
74
125
  name: 'List at least three products',
75
126
  required: true,
76
127
  preferences: { minimum_count: 3 },
77
128
  })
78
129
  ```
79
130
 
131
+ ```bash cURL
132
+ # Requirement kinds this marketplace offers
133
+ curl 'https://api.mystore.com/api/v3/admin/seller_requirements/types' \
134
+ -H 'X-Spree-API-Key: sk_xxx'
135
+
136
+ # Add one to the checklist
137
+ curl -X POST 'https://api.mystore.com/api/v3/admin/seller_requirements' \
138
+ -H 'X-Spree-API-Key: sk_xxx' \
139
+ -H 'Content-Type: application/json' \
140
+ -d '{
141
+ "type": "minimum_products",
142
+ "name": "List at least three products",
143
+ "required": true
144
+ }'
145
+ ```
146
+
147
+
80
148
  Requirements come in three flavours, which is what lets one mechanism cover very different demands:
81
149
 
82
150
  | Kind | How it's satisfied |
@@ -89,6 +157,7 @@ Shipped out of the box: accepting terms, completing the profile, a billing addre
89
157
 
90
158
  A seller sees their checklist and its progress:
91
159
 
160
+
92
161
  ```typescript Seller SDK
93
162
  const onboarding = await sellerClient.onboarding.get()
94
163
 
@@ -102,6 +171,13 @@ onboarding.requirements.forEach((r) => {
102
171
  await sellerClient.onboarding.submitForReview()
103
172
  ```
104
173
 
174
+ ```bash cURL
175
+ curl 'https://marketplace.example.com/api/v3/seller/onboarding' \
176
+ -H 'Authorization: Bearer $SELLER_JWT' \
177
+ -H 'X-Spree-Seller-Id: sel_xxx'
178
+ ```
179
+
180
+
105
181
  The checklist is **worked out when read**, never stored on the seller. So a requirement added next month applies immediately to everyone, and nothing has to be backfilled.
106
182
 
107
183
  > **NOTE:** The checklist is enforced at exactly two moments — submitting for review, and approval. Approval refuses while a required item is outstanding, unless an operator deliberately overrides it.
@@ -114,11 +190,26 @@ A product can name a seller. No seller means it's the marketplace's own stock
114
190
 
115
191
  Sellers don't publish; they **submit**, and the marketplace decides. That review flow, and the statuses behind it, are covered in [Products](products.md#seller-submissions).
116
192
 
193
+
117
194
  ```typescript Seller SDK
118
195
  const product = await sellerClient.products.create({ name: 'Handmade Vase' })
119
196
  await sellerClient.products.submit(product.id)
120
197
  ```
121
198
 
199
+ ```bash cURL
200
+ # Create a product, then submit it for review
201
+ curl -X POST 'https://marketplace.example.com/api/v3/seller/products' \
202
+ -H 'Authorization: Bearer $SELLER_JWT' \
203
+ -H 'X-Spree-Seller-Id: sel_xxx' \
204
+ -H 'Content-Type: application/json' \
205
+ -d '{ "name": "Handmade Vase" }'
206
+
207
+ curl -X POST 'https://marketplace.example.com/api/v3/seller/products/prod_xxx/submit' \
208
+ -H 'Authorization: Bearer $SELLER_JWT' \
209
+ -H 'X-Spree-Seller-Id: sel_xxx'
210
+ ```
211
+
212
+
122
213
  Stores that trust their vendors can turn auto-approval on and skip the queue.
123
214
 
124
215
  ## One checkout, several sellers
@@ -156,12 +247,19 @@ Two details worth knowing:
156
247
 
157
248
  Commission is what the marketplace charges for the sale — configured as **rates**, and recorded per sale as immutable **commission lines**.
158
249
 
250
+
159
251
  ```typescript Admin SDK
160
252
  const { data: lines } = await adminClient.commissionLines.list({
161
- filter: { seller_id_eq: 'sel_xxx' },
253
+ seller_id_eq: 'sel_xxx',
162
254
  })
163
255
  ```
164
256
 
257
+ ```bash cURL
258
+ curl 'https://api.mystore.com/api/v3/admin/commission_lines?q[seller_id_eq]=sel_xxx' \
259
+ -H 'X-Spree-API-Key: sk_xxx'
260
+ ```
261
+
262
+
165
263
  Three things are worth knowing here; the rest is on its own page.
166
264
 
167
265
  - **Rates are tried in list order**, and the first whose rules match wins. A rate with no rules matches everything, so the marketplace default belongs at the bottom.
@@ -184,6 +282,7 @@ Sellers get their own application — not access to the marketplace's dashboard.
184
282
 
185
283
  That last point is the security property worth relying on: a seller cannot ask for another seller's data, because there's nowhere in the request to name one.
186
284
 
285
+
187
286
  ```typescript Seller SDK
188
287
  import { createSellerClient } from '@spree/seller-sdk'
189
288
 
@@ -197,6 +296,14 @@ await sellerClient.orders.fulfillments.fulfill(orderId, fulfillmentId, {
197
296
  })
198
297
  ```
199
298
 
299
+ ```bash cURL
300
+ # Sign in, then use the returned JWT on every seller call
301
+ curl -X POST 'https://marketplace.example.com/api/v3/seller/auth/login' \
302
+ -H 'Content-Type: application/json' \
303
+ -d '{ "email": "vendor@example.com", "password": "…" }'
304
+ ```
305
+
306
+
200
307
  Sellers can manage their profile, invite their own team, list and submit products, see and fulfil their orders, manage their stock locations, delivery methods and packaging, and work through onboarding. They cannot see other sellers, the marketplace's own catalog, or anything belonging to the store at large.
201
308
 
202
309
  What a seller's staff may do is governed by [roles](staff-roles.md) owned by the seller — the same permission system as the back office, with a narrower set of keys, so a seller role can never reach store settings.
@@ -59,7 +59,7 @@ If a slug already exists, Spree appends the SKU or a unique identifier to ensure
59
59
 
60
60
  Category slugs include the full parent path, making them hierarchical:
61
61
 
62
- ```
62
+ ```text
63
63
  clothing → "clothing"
64
64
  clothing/shirts → "clothing/shirts"
65
65
  clothing/shirts/t-shirts → "clothing/shirts/t-shirts"
@@ -44,6 +44,7 @@ Because roles are scoped to their owner, two stores can each define a "Manager"
44
44
 
45
45
  A permission key is a verb and a resource — `read_orders`, `write_products`. Roles hold a list of them:
46
46
 
47
+
47
48
  ```typescript Admin SDK
48
49
  const role = await adminClient.roles.create({
49
50
  name: 'Fulfillment staff',
@@ -52,12 +53,31 @@ const role = await adminClient.roles.create({
52
53
  })
53
54
  ```
54
55
 
56
+ ```bash cURL
57
+ curl -X POST 'https://api.mystore.com/api/v3/admin/roles' \
58
+ -H 'X-Spree-API-Key: sk_xxx' \
59
+ -H 'Content-Type: application/json' \
60
+ -d '{
61
+ "name": "Fulfillment staff",
62
+ "description": "Can see orders and ship them, nothing else",
63
+ "permissions": ["read_orders", "write_fulfillments", "read_products"]
64
+ }'
65
+ ```
66
+
67
+
55
68
  Every grantable key is discoverable, so a permission picker never needs a hardcoded list:
56
69
 
70
+
57
71
  ```typescript Admin SDK
58
72
  const { data: permissions } = await adminClient.permissions.list()
59
73
  ```
60
74
 
75
+ ```bash cURL
76
+ curl 'https://api.mystore.com/api/v3/admin/permissions' \
77
+ -H 'X-Spree-API-Key: sk_xxx'
78
+ ```
79
+
80
+
61
81
  Keys are grouped so they can be presented sensibly — orders, catalog, marketing, customers, settings, access and analytics.
62
82
 
63
83
  | Group | Covers |
@@ -84,13 +104,13 @@ A role is deletable only when nothing depends on it: staff assignments and pendi
84
104
 
85
105
  Use the Spree CLI to create admin users:
86
106
 
87
- ```bash
107
+ ```bash Spree CLI
88
108
  spree user create
89
109
  ```
90
110
 
91
111
  The CLI will prompt you for the email and password. You can also pass them directly:
92
112
 
93
- ```bash
113
+ ```bash Spree CLI
94
114
  spree user create --email admin@example.com --password secret123
95
115
  ```
96
116
 
@@ -140,7 +160,7 @@ You can invite new admins through the Admin Panel or programmatically.
140
160
 
141
161
  Using the [Admin SDK](../sdk/admin/quickstart.md), call `client.invitations.create`:
142
162
 
143
- ```typescript
163
+ ```typescript Admin SDK
144
164
  import { createAdminClient } from '@spree/admin-sdk'
145
165
 
146
166
  const client = createAdminClient({
@@ -74,26 +74,22 @@ erDiagram
74
74
  | `currency` | Currency code | `USD` |
75
75
  | `memo` | Optional note about the credit | `Refund for order #R123` |
76
76
 
77
- ### Store Credit Categories
77
+ ### Why a Credit Exists
78
78
 
79
- Categories help organize store credits by their purpose:
79
+ Store credits carry no category and no type — both were retired in Spree 6.0
80
+ because neither answered a question anything asked. Two fields cover it
81
+ instead:
80
82
 
81
- | Category | Description |
82
- |----------|-------------|
83
- | Default | General purpose store credit (also the default reimbursement category) |
84
- | Expiring | Store credit that may expire |
85
- | Non-expiring | Store credit that does not expire |
83
+ | Field | What it tells you |
84
+ |-------|-------------------|
85
+ | `originator` | The return, exchange, claim or gift card that issued the credit, or nothing when an admin issued it by hand |
86
+ | `memo` | The free-text reason, written by the refund workflows or by the admin |
86
87
 
87
- > **INFO:** "Gift Card" is not a seeded category but a special name recognized by the model: any category named "Gift Card" (or one listed in `Spree::Config[:non_expiring_credit_types]`) is treated as non-expiring via `StoreCreditCategory#non_expiring?`.
88
+ Store credits do not expire; only gift cards carry an expiry, on their own
89
+ record. Which credit is spent first is decided by `Spree::StoreCredit.oldest_first`.
88
90
 
89
- ### Store Credit Types
90
-
91
- Types define the priority in which store credits are applied at checkout:
92
-
93
- | Type | Description |
94
- |------|-------------|
95
- | Expiring | Credits that may expire (applied first) |
96
- | Non-expiring | Credits that don't expire (applied last) |
91
+ > **NOTE:** `Spree::StoreCreditCategory` and `Spree::StoreCreditType` survive as deprecated
92
+ > shells for one release and are removed in 6.1. Nothing writes them.
97
93
 
98
94
  ### Store Credit Events
99
95
 
@@ -115,7 +111,7 @@ Store credits are managed in the Admin Panel:
115
111
  2. Select a customer
116
112
  3. Go to the **Store Credits** tab
117
113
  4. Click **Add Store Credit**
118
- 5. Enter the amount, category, and optional memo
114
+ 5. Enter the amount, currency, and optional memo
119
115
  6. Click **Create**
120
116
 
121
117
  Store credits can also be created via the [Admin API](../../api-reference/admin-api/introduction.md), as a nested resource under the customer:
@@ -132,7 +128,6 @@ const client = createAdminClient({
132
128
  const credit = await client.customers.storeCredits.create('cus_xxx', {
133
129
  amount: '25.00',
134
130
  currency: 'USD',
135
- category_id: 'sccat_xxx',
136
131
  memo: 'Goodwill credit for delayed shipment',
137
132
  })
138
133
  ```
@@ -141,12 +136,55 @@ const credit = await client.customers.storeCredits.create('cus_xxx', {
141
136
  spree api post /customers/cus_xxx/store_credits -d '{
142
137
  "amount": "25.00",
143
138
  "currency": "USD",
144
- "category_id": "sccat_xxx",
145
139
  "memo": "Goodwill credit"
146
140
  }'
147
141
  ```
148
142
 
149
143
 
144
+ ### Listing Store Credits Across Customers
145
+
146
+ Credits are written per customer, but read across them. The Admin API exposes a
147
+ read-only list so an integration can answer what the store owes without walking
148
+ every customer:
149
+
150
+
151
+ ```typescript Admin SDK
152
+ const { data: credits, meta } = await client.storeCredits.list({
153
+ outstanding: true,
154
+ expand: ['customer', 'created_by'],
155
+ })
156
+
157
+ // One row per currency, summed over the same filter as the page.
158
+ meta.totals.forEach((total) => {
159
+ console.log(total.currency, total.display_amount_remaining)
160
+ })
161
+ ```
162
+
163
+ ```bash CLI
164
+ spree api get /store_credits -q 'outstanding=true'
165
+ ```
166
+
167
+
168
+ Filters mirror the questions a merchant asks: `customer_id`,
169
+ `customer_email_cont`, `currency`, `created_by_id`, `memo_cont`, a
170
+ `created_at` range, and two scopes — `outstanding` (money still owed versus
171
+ money spent) and `from_gift_card` (whether a gift card redemption created it).
172
+
173
+ Each credit's ledger is its own endpoint:
174
+
175
+
176
+ ```typescript Admin SDK
177
+ const { data: events } = await client.storeCredits.events.list('credit_xxx')
178
+ ```
179
+
180
+ ```bash CLI
181
+ spree api get /store_credits/credit_xxx/events
182
+ ```
183
+
184
+
185
+ Creating, updating and deleting a credit remain nested under the customer that
186
+ holds it — a credit without an owner has nobody to pay.
187
+
150
188
  ### Store Credit Events
151
189
 
152
190
  The store credit system publishes lifecycle events:
@@ -387,10 +425,9 @@ flowchart TB
387
425
 
388
426
  ### Store Credit Priority
389
427
 
390
- When a registered customer has multiple store credits, they are applied in order of priority:
391
-
392
- 1. **Expiring credits** - Applied first to ensure they're used before expiration
393
- 2. **Non-expiring credits** - Applied after expiring credits
428
+ When a registered customer holds several store credits, the oldest is spent
429
+ first (`Spree::StoreCredit.oldest_first`). Store credits do not expire — only
430
+ gift cards carry an expiry, on their own record.
394
431
 
395
432
  ## Related Documentation
396
433
 
@@ -7,6 +7,27 @@ description: Understand Spree Stores — the top-level tenant boundary that scop
7
7
 
8
8
  The Store is the top-level tenant in Spree. Every resource — products, orders, channels, markets, taxonomies — belongs to exactly one store. A store owns its [channels](channels.md) (online, POS, wholesale, …), its [markets](markets.md) (region/currency/locale), and its [product catalog](products.md).
9
9
 
10
+ ```mermaid
11
+ erDiagram
12
+ Store ||--o{ Channel : "sells through"
13
+ Store ||--o{ Market : "trades in"
14
+ Store ||--o{ Product : "catalogs"
15
+ Store ||--o{ Order : "records"
16
+ Store ||--o{ PaymentMethod : "accepts"
17
+ Store ||--o{ StockLocation : "ships from"
18
+ Store ||--o{ Policy : "publishes"
19
+ Store ||--o{ Seller : "hosts"
20
+
21
+ Store {
22
+ string name
23
+ string code
24
+ string url
25
+ string default_currency
26
+ string default_locale
27
+ boolean default
28
+ }
29
+ ```
30
+
10
31
  ## Store Attributes
11
32
 
12
33
  | Attribute | Description |
@@ -36,6 +36,7 @@ Every product carries one. If you don't choose, the default is used.
36
36
  | `is_default` | Used by products that don't pick one | `true` |
37
37
  | `tax_code` | The code your tax service knows it by | `1257L` |
38
38
 
39
+
39
40
  ```typescript Admin SDK
40
41
  const reduced = await adminClient.taxCategories.create({
41
42
  name: 'Reduced rate',
@@ -43,6 +44,14 @@ const reduced = await adminClient.taxCategories.create({
43
44
  })
44
45
  ```
45
46
 
47
+ ```bash cURL
48
+ curl -X POST 'https://api.mystore.com/api/v3/admin/tax_categories' \
49
+ -H 'X-Spree-API-Key: sk_xxx' \
50
+ -H 'Content-Type: application/json' \
51
+ -d '{ "name": "Reduced rate", "tax_code": "1257L" }'
52
+ ```
53
+
54
+
46
55
  `tax_code` matters once you connect an external service — it is how you tell that service what kind of thing this is, in the vocabulary it already uses.
47
56
 
48
57
  ## Tax rates
@@ -57,6 +66,7 @@ A tax rate is the number itself, plus where it applies and whether it's already
57
66
  | `country_code` / `state_code` | Where it applies | `DE`, or `US` + `CA` |
58
67
  | `tax_category` | What it applies to | `Standard` |
59
68
 
69
+
60
70
  ```typescript Admin SDK
61
71
  const { data: rates } = await adminClient.taxRates.list()
62
72
 
@@ -69,6 +79,25 @@ await adminClient.taxRates.create({
69
79
  })
70
80
  ```
71
81
 
82
+ ```bash cURL
83
+ # List tax rates
84
+ curl 'https://api.mystore.com/api/v3/admin/tax_rates' \
85
+ -H 'X-Spree-API-Key: sk_xxx'
86
+
87
+ # Create one
88
+ curl -X POST 'https://api.mystore.com/api/v3/admin/tax_rates' \
89
+ -H 'X-Spree-API-Key: sk_xxx' \
90
+ -H 'Content-Type: application/json' \
91
+ -d '{
92
+ "name": "VAT 20%",
93
+ "amount": "0.2",
94
+ "included_in_price": true,
95
+ "country_code": "GB",
96
+ "tax_category_id": "taxcat_xxx"
97
+ }'
98
+ ```
99
+
100
+
72
101
  Rates name their country and state by **ISO code**, not by an ID — `DE`, `US` + `CA`. There's no separate region record to create first.
73
102
 
74
103
  ## Included or added on
@@ -120,10 +149,17 @@ Whatever works out the tax, the record is the same: a tax line per charge, sayin
120
149
  | `amount` / `display_amount` | The tax charged |
121
150
  | `line_item_id` / `fulfillment_id` / `fee_id` | What was taxed — exactly one is set |
122
151
 
152
+
123
153
  ```typescript Admin SDK
124
154
  const { data: taxLines } = await adminClient.orders.taxLines.list('or_xxx')
125
155
  ```
126
156
 
157
+ ```bash cURL
158
+ curl 'https://api.mystore.com/api/v3/admin/orders/or_xxx/tax_lines' \
159
+ -H 'X-Spree-API-Key: sk_xxx'
160
+ ```
161
+
162
+
127
163
  Tax lines attach to a line item, a [fulfillment](fulfillments.md), or a [fee](fees.md) — so tax on goods and tax on delivery stay separately visible, which is what a tax return needs.
128
164
 
129
165
  Each line also keeps its own copy of the rate and label. If someone edits that tax rate next year, the order still says what the customer was actually charged.
@@ -153,10 +189,17 @@ Spree's built-in rate tables are fine when the rules are simple — one country,
153
189
 
154
190
  For those, connect a real tax service. **The provider is chosen per [Market](markets.md)**, which means one store can use its own rates in a simple market and an external service in a complicated one.
155
191
 
192
+
156
193
  ```typescript Admin SDK
157
194
  const { data: providers } = await adminClient.taxProviders.list()
158
195
  ```
159
196
 
197
+ ```bash cURL
198
+ curl 'https://api.mystore.com/api/v3/admin/tax_providers' \
199
+ -H 'X-Spree-API-Key: sk_xxx'
200
+ ```
201
+
202
+
160
203
  A provider declares up front what it cannot do — no US local tax, no reverse charge — so the dashboard can warn a merchant who pairs it with a market that needs it, rather than quietly under-collecting.
161
204
 
162
205
  Whichever provider is in use, the result is the same: tax lines on the order, in the same shape. Your storefront code doesn't change.
@@ -87,6 +87,7 @@ Merchants translate in the dashboard, switching locale on the record they're edi
87
87
 
88
88
  Translations can also be written through the Admin API, which is what you'd use to sync from an external translation service:
89
89
 
90
+
90
91
  ```typescript Admin SDK
91
92
  await adminClient.products.update('prod_xxx', {
92
93
  translations: {
@@ -99,6 +100,14 @@ await adminClient.products.update('prod_xxx', {
99
100
  const { data: resources } = await adminClient.translatableResources.list()
100
101
  ```
101
102
 
103
+ ```bash cURL
104
+ curl -X PATCH 'https://api.mystore.com/api/v3/admin/products/prod_xxx' \
105
+ -H 'X-Spree-API-Key: sk_xxx' \
106
+ -H 'Content-Type: application/json' \
107
+ -d '{ "translations": { "de": { "name": "Baumwolltasche" } } }'
108
+ ```
109
+
110
+
102
111
  Asking the API which resources are translatable means a translation tool doesn't need a hardcoded list that goes stale.
103
112
 
104
113
  ## Translating the interface