@spree/docs 0.1.304 → 0.1.306

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.
@@ -8,18 +8,17 @@ All monetary values in the Admin API are **canonical decimal strings** (period d
8
8
 
9
9
  ## Reading
10
10
 
11
- Every monetary field is returned as a string, alongside a `display_` companion that includes currency formatting:
11
+ Every monetary field is returned as a decimal string, with its currency on the record or on the record it belongs to (an order's payments and fulfillments are in the order's currency):
12
12
 
13
13
  ```json
14
14
  {
15
15
  "amount": "29.99",
16
- "display_amount": "$29.99",
17
16
  "compare_at_amount": "39.99",
18
- "display_compare_at_amount": "$39.99"
17
+ "currency": "USD"
19
18
  }
20
19
  ```
21
20
 
22
- Use `display_*` for rendering and the raw string fields for calculations, with a decimal library rather than `parseFloat`. The SDK exports `sumMoney`, `subtractMoney`, `multiplyMoney`, `compareMoney` and `isZeroMoney`, which work on the strings exactly.
21
+ The Admin and Seller APIs return amounts only, with no `display_*` formatted copies: format them for the person reading them, in their own language. In JavaScript, pass the string to `Intl.NumberFormat` (`new Intl.NumberFormat('de', { style: 'currency', currency: 'EUR' }).format('1234.5')` gives `1.234,50 €`), which reads a numeric string exactly. Calculate with a decimal library rather than `parseFloat`: the SDK exports `sumMoney`, `subtractMoney`, `multiplyMoney`, `compareMoney` and `isZeroMoney`, which work on the strings exactly. The [Store API](../store-api/monetary-amounts.md) keeps `display_*` fields for storefronts, and webhook payloads and email templates carry them too.
23
22
 
24
23
  ## Writing
25
24
 
@@ -11854,50 +11854,21 @@ components:
11854
11854
  - id
11855
11855
  - name
11856
11856
  - code
11857
- PreferenceField:
11857
+ PreferenceSchema:
11858
11858
  type: object
11859
- description: A single configurable preference on a payment method, promotion
11860
- rule/action, or calculator. The frontend uses `type` + `default` to render
11861
- a sensible input.
11862
- properties:
11863
- key:
11864
- type: string
11865
- example: amount_min
11866
- type:
11867
- type: string
11868
- example: decimal
11869
- description: string | text | password | integer | decimal | boolean | array
11870
- | hash
11871
- default:
11872
- description: Default value (any JSON type), null when there is no default
11873
- nullable: true
11874
- required:
11875
- - key
11876
- - type
11877
- PromotionActionCalculator:
11878
- type: object
11879
- description: The action's nested calculator (when the action carries one — null
11880
- for actions like `free_shipping`)
11881
- properties:
11882
- type:
11883
- type: string
11884
- example: flat_rate
11885
- description: Wire shorthand for the calculator subclass
11886
- label:
11887
- type: string
11888
- example: Flat Rate
11889
- preferences:
11890
- type: object
11891
- additionalProperties: true
11892
- preference_schema:
11893
- type: array
11894
- items:
11895
- "$ref": "#/components/schemas/PreferenceField"
11896
- required:
11897
- - type
11898
- - label
11899
- - preferences
11900
- - preference_schema
11859
+ description: 'The JSON Schema (draft 2020-12) of a type''s `preferences`: one
11860
+ property per setting, with `format` (`money`, `currency`, `iso-country`, `prefixed-id`,
11861
+ …), `enum`, `default`, `x-spree-secret` for a secret and `x-spree-prefix`
11862
+ for an id list. Unknown keys are refused.'
11863
+ additionalProperties: true
11864
+ example:
11865
+ type: object
11866
+ properties:
11867
+ amount:
11868
+ type: string
11869
+ format: money
11870
+ default: '0.0'
11871
+ additionalProperties: false
11901
11872
  PromotionActionLineItem:
11902
11873
  type: object
11903
11874
  description: One row in a `create_line_items` action — the variant added to
@@ -94,8 +94,8 @@ const calculators = await adminClient.deliveryMethods.calculators()
94
94
  await adminClient.deliveryMethods.create({
95
95
  name: 'Standard shipping',
96
96
  delivery_zone_id: 'dz_xxx',
97
- calculator_type: 'flat_rate',
98
- calculator_attributes: {
97
+ calculator: {
98
+ type: 'flat_rate',
99
99
  preferences: { amount: '7.00', currency: 'USD' },
100
100
  },
101
101
  })
@@ -38,7 +38,7 @@ erDiagram
38
38
  | `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` |
39
39
  | `storefront_access` | Controls what an anonymous visitor may see: `public`, `prices_hidden`, or `login_required`. Unset inherits the store's setting. See [Storefront Access Gating](#storefront-access-gating) | `login_required` |
40
40
  | `guest_checkout` | Whether an order may be placed without an account on this channel. Unset inherits the store's setting | `false` |
41
- | `preferred_order_routing_strategy` | Optional per-channel override of the store's [Order Routing](../how-to/custom-order-routing.md) strategy | `rules` |
41
+ | `order_routing_strategy` | Optional per-channel override of the store's [Order Routing](../how-to/custom-order-routing.md) strategy | `rules` |
42
42
 
43
43
  `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`.
44
44
 
@@ -132,13 +132,13 @@ Both controls fall back to the owning [Store](stores.md) when the channel's own
132
132
  ```typescript Admin SDK
133
133
  // Gate a channel behind sign-in, and require accounts at checkout
134
134
  await adminClient.channels.update('ch_wholesale', {
135
- preferred_storefront_access: 'login_required',
136
- preferred_guest_checkout: false,
135
+ storefront_access: 'login_required',
136
+ guest_checkout: false,
137
137
  })
138
138
 
139
139
  // Clear the channel value to inherit the store's default
140
140
  await adminClient.channels.update('ch_wholesale', {
141
- preferred_storefront_access: null,
141
+ storefront_access: null,
142
142
  })
143
143
  ```
144
144
 
@@ -146,7 +146,7 @@ await adminClient.channels.update('ch_wholesale', {
146
146
  curl -X PATCH 'https://api.mystore.com/api/v3/admin/channels/ch_wholesale' \
147
147
  -H 'X-Spree-API-Key: sk_xxx' \
148
148
  -H 'Content-Type: application/json' \
149
- -d '{ "preferred_storefront_access": "login_required", "preferred_guest_checkout": false }'
149
+ -d '{ "storefront_access": "login_required", "guest_checkout": false }'
150
150
  ```
151
151
 
152
152
 
@@ -295,14 +295,14 @@ Price history tracking is enabled by default. To disable it (e.g., for non-EU st
295
295
 
296
296
 
297
297
  ```typescript Admin SDK
298
- await client.store.update({ preferred_track_price_history: false })
298
+ await client.store.update({ track_price_history: false })
299
299
  ```
300
300
 
301
301
  ```bash cURL
302
302
  curl -X PATCH 'https://api.mystore.com/api/v3/admin/store' \
303
303
  -H 'X-Spree-API-Key: sk_xxx' \
304
304
  -H 'Content-Type: application/json' \
305
- -d '{ "preferred_track_price_history": false }'
305
+ -d '{ "track_price_history": false }'
306
306
  ```
307
307
 
308
308
 
@@ -39,6 +39,137 @@ end
39
39
 
40
40
  Any model inheriting from `Spree.base_class` can declare preferences — the reader and writer methods come with it, so there is nothing to include. The values live in that model's `preferences` column.
41
41
 
42
+ ### Declaring the full type
43
+
44
+ A declaration is also the preference's contract in the API: Spree turns it into a JSON Schema that the API publishes, the SDKs type and every write is checked against. So a declaration states everything a client needs to know about the value:
45
+
46
+ ```ruby server/app/models/my_app/promotion/rules/loyalty_tier.rb
47
+ module MyApp
48
+ module Promotion
49
+ module Rules
50
+ class LoyaltyTier < Spree::PromotionRule
51
+ preference :minimum_spend, :money, default: 0
52
+ preference :tier, :string, choices: %w[silver gold platinum], default: 'silver'
53
+ preference :channel_ids, :array, of: :id, model: 'Spree::Channel', default: [],
54
+ scope: ->(rule) { rule.promotion.store.channels }
55
+ preference :country_codes, :array, of: :string, format: :iso_country, default: []
56
+ preference :bonus_amounts, :hash, keys: :currency, values: :money, default: {}
57
+ end
58
+ end
59
+ end
60
+ end
61
+ ```
62
+
63
+ | Option | Applies to | What it says |
64
+ |---|---|---|
65
+ | `of:` | `:array` | The type of each item: `:string`, `:integer`, `:decimal`, `:money`, `:boolean`, `:id` or `:object` |
66
+ | `properties:` | `of: :object` | The fields of each item, e.g. `{ threshold: :money, value: :decimal }` |
67
+ | `keys:` / `values:` | `:hash` | The type of the keys (`:string` or `:currency`) and of the values |
68
+ | `model:` / `scope:` | `of: :id` | The records the ids point at, and the records they must be found in. The API accepts and returns their prefixed ids (`ch_…`); an id of another model, or one outside the scope, is refused |
69
+ | `format:` | `:string`, list items | `:currency`, `:iso_country`, `:timezone`, `:locale`, `:url` or `:color` |
70
+ | `choices:` | any | The fixed set the value must come from; admin forms show a picker |
71
+
72
+ Decimals and amounts travel as exact strings (`"9.99"`), never as numbers. An `:array` without `of:`, a `:hash` without `keys:` and `values:`, or the `:any` type still works in Spree 6.0, with a deprecation warning and a schema that accepts any value. Spree 6.1 refuses them.
73
+
74
+ ### Preferences in the API
75
+
76
+ Configurable types — promotion rules and actions, calculators, payment methods, integrations, and price, delivery, commission and order routing rules — send their settings as a `preferences` object next to their `type`. Each family's `/types` endpoint returns the JSON Schema of every registered type's preferences, extensions included, so a client can render a form for a type it has never seen:
77
+
78
+
79
+ ```typescript Admin SDK
80
+ const { data } = await adminClient.promotionRules.types()
81
+ const itemTotal = data.find((entry) => entry.type === 'item_total')
82
+ itemTotal.schema.properties.amount_min // { type: 'string', format: 'money', default: '100', … }
83
+ ```
84
+
85
+ ```bash cURL
86
+ curl 'https://api.mystore.com/api/v3/admin/promotion_rules/types' \
87
+ -H 'X-Spree-API-Key: sk_xxx'
88
+ ```
89
+
90
+
91
+ The SDKs also type the settings of every type Spree ships. `TypedPromotionRule` narrows `preferences` by `type`, and an extension adds its own types to `PromotionRulePreferencesMap` by declaration merging.
92
+
93
+ A write is checked against the schema before anything is saved. An unknown key, a value of the wrong type, a value outside `choices`, or an id of the wrong model or another store is refused with a `422` whose `code` is `invalid_preferences`; `details` names each failing value by its JSON pointer, such as `/preferences/channel_ids/1`. A secret reads back masked; sending the masked value back keeps the stored secret, and `null` clears it.
94
+
95
+ ### Exposing a setting in the API
96
+
97
+ Use this when a record has a fixed set of settings that clients edit field by field, as the store, a channel or an import do. A family of types with different settings per type (rules, calculators, payment methods) sends a `preferences` object instead, as described above.
98
+
99
+ The API names such a setting by its plain name (`featured`), never by the `preferred_` method. It takes three declarations, one per layer, and each layer stays in charge of its own job.
100
+
101
+ **1. Expose it on the model.** `exposes_preferences` gives each preference a plain reader, writer and predicate:
102
+
103
+ ```ruby server/app/models/spree/brand.rb
104
+ module Spree
105
+ class Brand < Spree.base_class
106
+ preference :featured, :boolean, default: false
107
+ preference :display_name, :string
108
+
109
+ exposes_preferences :featured, :display_name
110
+ end
111
+ end
112
+ ```
113
+
114
+ ```ruby
115
+ brand.featured # same as brand.preferred_featured
116
+ brand.featured = true # same as brand.preferred_featured = true
117
+ brand.featured? # true
118
+ ```
119
+
120
+ It refuses a name the model already uses for something else. Errors on `preferred_featured` are reported to API clients as `featured`.
121
+
122
+ **2. Return it from the serializer.** `preference_attributes` adds every exposed preference, typed from its declaration, so the generated SDK types follow the model. Name some to add only those, or pass `except:` to leave some out:
123
+
124
+ ```ruby server/app/serializers/spree/api/v3/admin/brand_serializer.rb
125
+ module Spree
126
+ module Api
127
+ module V3
128
+ module Admin
129
+ class BrandSerializer < Spree::Api::V3::BaseSerializer
130
+ attributes :name, created_at: :iso8601, updated_at: :iso8601
131
+
132
+ preference_attributes Spree::Brand
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
138
+ ```
139
+
140
+ **3. Accept it in the controller.** Exposing a setting does not make it writable: the controller decides, as for any attribute (see [permitted attributes](api.md#permitted-attributes)):
141
+
142
+ ```ruby server/app/controllers/spree/api/v3/admin/brands_controller.rb
143
+ def resource_permitted_attributes
144
+ [:name, :featured, :display_name]
145
+ end
146
+ ```
147
+
148
+ The setting is then read and written like a column:
149
+
150
+ **Admin SDK:**
151
+
152
+ ```typescript
153
+ const brand = await adminClient.request('PATCH', `/brands/${brandId}`, {
154
+ body: { featured: true, display_name: 'Wilson' },
155
+ })
156
+ brand.featured // true
157
+ ```
158
+
159
+ **cURL:**
160
+
161
+ ```bash
162
+ curl -X PATCH 'https://api.mystore.com/api/v3/admin/brands/brand_xxx' \
163
+ -H 'X-Spree-API-Key: sk_xxx' \
164
+ -H 'Content-Type: application/json' \
165
+ -d '{ "featured": true, "display_name": "Wilson" }'
166
+ ```
167
+
168
+
169
+ > **NOTE:** The Seller API keeps its own list of what sellers may write, so exposing a setting to operators never makes it writable by sellers.
170
+
171
+ Strings, integers and booleans are cast the way Rails casts attributes (`"1"`, `"true"` and `"on"` are true; `"42"` is 42). A blank value is `nil` for a `nullable: true` preference, and the type's empty value otherwise.
172
+
42
173
  ## Accessing Model Preferences
43
174
 
44
175
  Once preferences have been defined for a model, they can be accessed either using the shortcut methods that are generated for each preference or the generic methods that are not specific to a particular preference.
@@ -96,6 +227,23 @@ This hash will contain the value for every preference that has been defined for
96
227
 
97
228
  `preferences` never holds secrets — see [Secret preferences](#secret-preferences).
98
229
 
230
+ ## Money preferences
231
+
232
+ Declare an amount of money with the `:money` type, and a rate or a measure (a percentage, a weight) with `:decimal`:
233
+
234
+ ```ruby server/app/models/spree/calculator/handling_fee.rb
235
+ module Spree
236
+ class Calculator::HandlingFee < Spree::Calculator
237
+ preference :amount, :money, default: 0
238
+ preference :currency, :string, default: -> { Spree::Store.default.default_currency }
239
+ preference :surcharge_percent, :decimal, default: 0
240
+ end
241
+ end
242
+ ```
243
+
244
+ Both are read back as a `BigDecimal` and take a number or a decimal string (`"4.50"`); text such as `"4,50"` is refused rather than misread. The difference is on the [API](../../api-reference/admin-api/monetary-amounts.md): a `:money` preference is written with the decimals of the record's `currency` preference (`"4.50"`, or `"450"` in yen), and the dashboard shows it with that currency's symbol. A money preference on a record with no currency, such as a rule matching an order total in any currency, keeps its exact decimal.
245
+
246
+
99
247
  ## Secret preferences
100
248
 
101
249
  Declare API keys, signing secrets and other credentials with the `:password` type:
@@ -129,4 +277,3 @@ Around fifty models use preferences, and the pattern is always the same: a famil
129
277
  | `Spree::Store`, `Spree::Channel` and `Spree::Market` | Merchant settings — timezone, units, currency behaviour |
130
278
 
131
279
  This is why preferences exist rather than columns: `Spree::Calculator::FlatRate` and `Spree::Calculator::TieredPercent` are rows in one table with entirely different settings, and neither wants a column the other leaves null.
132
- * `Spree::Theme`
@@ -73,7 +73,7 @@ dashboard under **Settings → Store**, or through the Admin API:
73
73
 
74
74
  ```typescript Admin SDK
75
75
  await adminClient.store.update({
76
- preferred_address_requires_phone: false,
76
+ address_requires_phone: false,
77
77
  })
78
78
  ```
79
79
 
@@ -81,7 +81,7 @@ await adminClient.store.update({
81
81
  curl -X PATCH 'https://api.mystore.com/api/v3/admin/store' \
82
82
  -H 'X-Spree-API-Key: sk_xxx' \
83
83
  -H 'Content-Type: application/json' \
84
- -d '{ "preferred_address_requires_phone": false }'
84
+ -d '{ "address_requires_phone": false }'
85
85
  ```
86
86
 
87
87
 
@@ -467,14 +467,14 @@ The gate is enforced by the Store API, not by the storefront, so a storefront ap
467
467
  ```typescript Admin SDK
468
468
  // Gate the wholesale channel, and give its shoppers a default agreement
469
469
  await adminClient.channels.update('ch_xxx', {
470
- preferred_storefront_access: 'login_required',
471
- preferred_guest_checkout: false,
470
+ storefront_access: 'login_required',
471
+ guest_checkout: false,
472
472
  default_catalog_id: 'cat_xxx',
473
473
  })
474
474
 
475
475
  // Or set the fallback for every channel that does not set its own
476
476
  await adminClient.store.update({
477
- preferred_storefront_access: 'prices_hidden',
477
+ storefront_access: 'prices_hidden',
478
478
  })
479
479
  ```
480
480
 
@@ -483,8 +483,8 @@ curl -X PATCH 'https://api.mystore.com/api/v3/admin/channels/ch_xxx' \
483
483
  -H 'X-Spree-API-Key: sk_xxx' \
484
484
  -H 'Content-Type: application/json' \
485
485
  -d '{
486
- "preferred_storefront_access": "login_required",
487
- "preferred_guest_checkout": false,
486
+ "storefront_access": "login_required",
487
+ "guest_checkout": false,
488
488
  "default_catalog_id": "cat_xxx"
489
489
  }'
490
490
  ```
@@ -50,15 +50,15 @@ Three store preferences decide how sellers get paid. All three are writable thro
50
50
 
51
51
  | Preference | Default | What it decides |
52
52
  |---|---|---|
53
- | `preferred_payout_provider` | blank | Who moves the money. Blank means core's record-only provider: the ledger is kept correctly and the operator settles by hand. |
54
- | `preferred_default_payouts_schedule_interval` | `monthly` | How often sellers are settled. One of `daily`, `weekly`, `biweekly`, `monthly`, `manual`. A seller can carry its own interval, which wins. |
55
- | `preferred_default_minimum_payout_amount` | `0` | What a seller's balance must reach before a settlement is worth sending. Below it the balance carries to the next period. |
53
+ | `payout_provider` | blank | Who moves the money. Blank means core's record-only provider: the ledger is kept correctly and the operator settles by hand. |
54
+ | `default_payouts_schedule_interval` | `monthly` | How often sellers are settled. One of `daily`, `weekly`, `biweekly`, `monthly`, `manual`. A seller can carry its own interval, which wins. |
55
+ | `default_minimum_payout_amount` | `0` | What a seller's balance must reach before a settlement is worth sending. Below it the balance carries to the next period. |
56
56
 
57
57
 
58
58
  ```typescript Admin SDK
59
59
  await adminClient.store.update({
60
- preferred_default_payouts_schedule_interval: 'weekly',
61
- preferred_default_minimum_payout_amount: 25,
60
+ default_payouts_schedule_interval: 'weekly',
61
+ default_minimum_payout_amount: '25.00',
62
62
  })
63
63
  ```
64
64
 
@@ -67,8 +67,8 @@ curl -X PATCH 'https://api.mystore.com/api/v3/admin/store' \
67
67
  -H 'X-Spree-API-Key: sk_xxx' \
68
68
  -H 'Content-Type: application/json' \
69
69
  -d '{
70
- "preferred_default_payouts_schedule_interval": "weekly",
71
- "preferred_default_minimum_payout_amount": 25
70
+ "default_payouts_schedule_interval": "weekly",
71
+ "default_minimum_payout_amount": "25.00"
72
72
  }'
73
73
  ```
74
74
 
@@ -88,18 +88,18 @@ Admin API like the payout ones.
88
88
 
89
89
  | Preference | Default | What it decides |
90
90
  |---|---|---|
91
- | `preferred_auto_approve_sellers` | `false` | Admit a seller the moment they finish the checklist, with nobody looking at them. |
92
- | `preferred_auto_approve_seller_products` | `false` | Put a seller's product on sale the moment they submit it. |
93
- | `preferred_send_seller_transactional_emails` | `true` | Whether Spree emails sellers. Turn it off if you front seller communications yourself. |
94
- | `preferred_default_commission_tax_rate` | `0` | Tax on your commission when neither the rate nor the tax provider names one. |
91
+ | `auto_approve_sellers` | `false` | Admit a seller the moment they finish the checklist, with nobody looking at them. |
92
+ | `auto_approve_seller_products` | `false` | Put a seller's product on sale the moment they submit it. |
93
+ | `send_seller_transactional_emails` | `true` | Whether Spree emails sellers. Turn it off if you front seller communications yourself. |
94
+ | `default_commission_tax_rate` | `0` | Tax on your commission when neither the rate nor the tax provider names one. |
95
95
 
96
96
 
97
97
  ```typescript Admin SDK
98
98
  await adminClient.store.update({
99
- preferred_auto_approve_sellers: false,
100
- preferred_auto_approve_seller_products: false,
101
- preferred_send_seller_transactional_emails: true,
102
- preferred_default_commission_tax_rate: 0.23,
99
+ auto_approve_sellers: false,
100
+ auto_approve_seller_products: false,
101
+ send_seller_transactional_emails: true,
102
+ default_commission_tax_rate: '0.23',
103
103
  })
104
104
  ```
105
105
 
@@ -108,10 +108,10 @@ curl -X PATCH 'https://api.mystore.com/api/v3/admin/store' \
108
108
  -H 'X-Spree-API-Key: sk_xxx' \
109
109
  -H 'Content-Type: application/json' \
110
110
  -d '{
111
- "preferred_auto_approve_sellers": false,
112
- "preferred_auto_approve_seller_products": false,
113
- "preferred_send_seller_transactional_emails": true,
114
- "preferred_default_commission_tax_rate": 0.23
111
+ "auto_approve_sellers": false,
112
+ "auto_approve_seller_products": false,
113
+ "send_seller_transactional_emails": true,
114
+ "default_commission_tax_rate": "0.23"
115
115
  }'
116
116
  ```
117
117
 
@@ -402,7 +402,7 @@ curl -X PATCH 'https://api.mystore.com/api/v3/admin/products/prod_xxx/reject' \
402
402
 
403
403
  Filter the review queue with a Ransack query on status: `adminClient.products.list({ status_eq: 'proposed' })`.
404
404
 
405
- With `preferred_auto_approve_seller_products` on, submitting chains straight into approval. The submission row still gets written and carries an `auto_approved` marker, so a blank reviewer reads as "this store does not review listings" rather than as a lost name.
405
+ With `auto_approve_seller_products` on, submitting chains straight into approval. The submission row still gets written and carries an `auto_approved` marker, so a blank reviewer reads as "this store does not review listings" rather than as a lost name.
406
406
 
407
407
  There is no bulk route onto `active` for sellers, and that is deliberate: reaching it is the operator's decision on one listing at a time.
408
408
 
@@ -467,7 +467,7 @@ curl 'https://api.mystore.com/api/v3/admin/commission_lines?q[seller_id_eq]=sel_
467
467
 
468
468
  There is no write path. Correcting a charge is a reversal, not an edit.
469
469
 
470
- In the EU the fee is a separate supply from the sale, so it is taxed separately — that is what `preferred_default_commission_tax_rate` from step 1 is for, and a rate or a tax provider can name its own.
470
+ In the EU the fee is a separate supply from the sale, so it is taxed separately — that is what `default_commission_tax_rate` from step 1 is for, and a rate or a tax provider can name its own.
471
471
 
472
472
  **Read more:** [Commissions](../core-concepts/commissions.md) covers the four rule types, gross-versus-net, how delivery is treated, currency floors and caps, and commission tax in full. [Commission rates](/user/sellers/commission-rates) is the operator's screen.
473
473
 
@@ -584,7 +584,7 @@ curl -X POST 'https://api.mystore.com/api/v3/admin/sellers/sel_xxx/payouts' \
584
584
 
585
585
  ### Payout providers
586
586
 
587
- A provider is a stateless class registered in `Spree.payout_providers` and chosen per store with `preferred_payout_provider`.
587
+ A provider is a stateless class registered in `Spree.payout_providers` and chosen per store with `payout_provider`.
588
588
 
589
589
  | Provider | Moves money? | Completion |
590
590
  |---|---|---|
@@ -246,7 +246,7 @@ Activate on the whole store:
246
246
 
247
247
  ```ruby
248
248
  Spree::Store.default.update!(
249
- preferred_order_routing_strategy: 'Acme::Oms::Strategy'
249
+ order_routing_strategy: 'Acme::Oms::Strategy'
250
250
  )
251
251
  ```
252
252
 
@@ -255,11 +255,11 @@ Override on one channel only:
255
255
  ```ruby
256
256
  store = Spree::Store.default
257
257
  store.channels.find_by(code: 'pos').update!(
258
- preferred_order_routing_strategy: 'Acme::Oms::Strategy'
258
+ order_routing_strategy: 'Acme::Oms::Strategy'
259
259
  )
260
260
  ```
261
261
 
262
- Resolution order: `channel.preferred_order_routing_strategy` → `store.preferred_order_routing_strategy` → the default `Strategy::Rules`. Only a value pointing to a **registered** `Strategy::Base` subclass is used; anything unset, unregistered, or invalid is skipped, so a misconfiguration (or a strategy you've since unregistered) falls back to the default instead of breaking checkout.
262
+ Resolution order: the channel's `order_routing_strategy` → the store's `order_routing_strategy` → the default `Strategy::Rules`. Only a value pointing to a **registered** `Strategy::Base` subclass is used; anything unset, unregistered, or invalid is skipped, so a misconfiguration (or a strategy you've since unregistered) falls back to the default instead of breaking checkout.
263
263
 
264
264
  ### Step 3: Test the Strategy
265
265
 
@@ -506,7 +506,34 @@ If your storefront or webhook receiver reads any of these fields, compare agains
506
506
  | `payment_source_type` | Store API payment setup sessions, `payment_setup_session.*` webhooks | `Spree::CreditCard` | `credit_card` |
507
507
  | `type` | Custom fields on products, variants, categories and collections | `Spree::CustomFields::ShortText` | removed — read `field_type` (`short_text`) |
508
508
 
509
- The Admin API follows the same rule everywhere: providers and strategies (`fulfillment_provider: 'manual'`, `preferred_order_routing_strategy: 'rules'`), custom field definition `resource_type` (`product`, `category`), tag `taggable_type`, the owner and originator types on records, the permission subjects `/me` returns (`product`, `category`), and filters such as `q[type_eq]=orders`. An integration written against the Admin API preview sends and expects these short names; the [Admin SDK](../sdk/admin/resources.md) types list them.
509
+ The Admin API follows the same rule everywhere: providers and strategies (`fulfillment_provider: 'manual'`, `order_routing_strategy: 'rules'`), custom field definition `resource_type` (`product`, `category`), tag `taggable_type`, the owner and originator types on records, the permission subjects `/me` returns (`product`, `category`), and filters such as `q[type_eq]=orders`. An integration written against the Admin API preview sends and expects these short names; the [Admin SDK](../sdk/admin/resources.md) types list them.
510
+
511
+ ## Preferences are typed in the API
512
+
513
+ Settings no longer reach the Admin and Seller APIs in the shape of Ruby's preference methods. The Store API is not affected.
514
+
515
+ **Settings use plain names.** Every `preferred_*` field is renamed, on read and on write:
516
+
517
+ | Resource | Before | After |
518
+ |---|---|---|
519
+ | Store (`/admin/store`) | `preferred_timezone`, `preferred_guest_checkout`, `preferred_storefront_url`, … every `preferred_*` field | `timezone`, `guest_checkout`, `storefront_url`, … the same name without the prefix |
520
+ | Channel | `preferred_storefront_access`, `preferred_guest_checkout`, `preferred_order_routing_strategy` | `storefront_access`, `guest_checkout`, `order_routing_strategy` |
521
+ | Import (Admin and Seller) | `preferred_delimiter` | `delimiter` |
522
+ | Seller profile | `preferred_timezone` | `timezone` |
523
+
524
+ `order.preferred_stock_location_id` keeps its name: it is an association, not a setting. Validation errors name the field without the prefix too. Writes still accept the old `preferred_*` names until Spree 6.1, with a deprecation warning in the server log; responses only carry the new names. The store's `storefront_url` is the setting as saved, `null` when unset; `url` is the address customer links use. The store's `default_minimum_payout_amount` and `default_commission_tax_rate` are now exact decimal strings (`"0.23"`) instead of numbers.
525
+
526
+ **`preference_schema` is replaced by `schema`.** Rules, actions, payment methods and integrations no longer carry `preference_schema` on every row. Each family's `/types` endpoint (and the delivery-method and promotion-action calculator lists) returns a `schema` per type instead: the JSON Schema of its `preferences`, with `format`, `enum`, `default` and `x-spree-secret`. Read a row's type, then look up its schema there. `/payment_methods/types` now lists every provider, with `installed: true` on those the store already has, instead of leaving them out.
527
+
528
+ **Writes are validated.** A `preferences` payload with an unknown key, a value of the wrong type, a value outside the declared choices, or an id of another model or another store is refused with a `422` whose `code` is `invalid_preferences`. `details` names each failing value by its JSON pointer (`/preferences/channel_ids/1`, or `/rules/0/preferences/channel_ids/1` inside a list of rules). Before, unknown keys were silently dropped. Send decimals as strings (`"10.0"`) and record ids as prefixed ids (`ch_…`).
529
+
530
+ **Id lists read as prefixed ids.** Preferences listing records, such as a rule's `channel_ids` or `customer_group_ids`, now return prefixed ids rather than database ids, the same form a write accepts.
531
+
532
+ **Delivery methods nest their calculator.** `calculator_type` and `calculator_preferences` are replaced by `calculator: { type, preferences }` on read and write, the shape promotion actions already use. Writes still accept the old two parameters until Spree 6.1, with a deprecation warning; their values follow the new rules (decimals as strings).
533
+
534
+ A preference marked deprecated, such as a flat rate calculator's `minimum_item_total`, is still accepted on write until its removal, with its deprecation warning, but is not read back.
535
+
536
+ **For extension authors:** declare every preference's full type (`of:` on arrays, `keys:` and `values:` on hashes, the `:money` type for amounts, `choices:`, `format:`). An incomplete declaration still works in 6.0 with a deprecation warning, and its schema accepts any value. Replace `normalize_id_preference` with `preference :ids, :array, of: :id, model:, scope:`, and `in:` with `choices:`. See [Model preferences](../customization/model-preferences.md#declaring-the-full-type).
510
537
 
511
538
  ## Every event is declared
512
539
 
@@ -582,7 +609,7 @@ Saving or touching a product used to bump the `updated_at` of each of its catego
582
609
 
583
610
  `Spree::OrderRouting::Strategy::Legacy` — the pre-5.5 escape hatch that delegated straight to `Spree::Stock::Coordinator` and consulted no routing rules — is removed and no longer registered in `Spree.order_routing.strategies`.
584
611
 
585
- A store or channel still carrying `preferred_order_routing_strategy: 'Spree::OrderRouting::Strategy::Legacy'` **keeps working**: `Order#order_routing_strategy` ignores unregistered classes, logs a warning, and falls back to `Strategy::Rules`. Clear the stale preference to silence the warning — note that saving such a record now fails validation, since the value is no longer in the registry.
612
+ A store or channel still carrying `order_routing_strategy: 'Spree::OrderRouting::Strategy::Legacy'` **keeps working**: `Order#order_routing_strategy` ignores unregistered classes, logs a warning, and falls back to `Strategy::Rules`. Clear the stale preference to silence the warning — note that saving such a record now fails validation, since the value is no longer in the registry.
586
613
 
587
614
  `Spree::Stock::Coordinator` itself stays — cart fulfillment building, exchanges, and claims still use it.
588
615
 
@@ -800,9 +827,11 @@ Other changes that come with it:
800
827
 
801
828
  - **Numbers are no longer parsed by locale.** Price, variant, payment, refund, store credit, gift card, discount and fee amounts take a number or a canonical decimal string (`"1234.56"`); any other text raises `Spree::Money::InvalidFormat`, which the API answers with `invalid_money_format`. Before 6.0 they were re-read under the request's language, so a store whose language writes a comma decimal (Dutch, German, French) saved the dashboard's `49.50` as 4950, and an unchanged price grew tenfold on each save. Send canonical decimal strings (`"1234.56"`) and convert localized input in the client. `Spree::LocalizedNumber` still works with a warning until 6.1. If your prices were saved in a comma-decimal store before upgrading, check them.
802
829
  - **Rounding follows the currency.** Taxes, percentage discounts, promotion splits and shipping markups round to the currency's own decimal places from the ISO 4217 table: whole yen, three decimals for dinar. Two-decimal currencies are unchanged. Open carts in other currencies may change by less than one minor unit the next time they are recalculated. Placed orders do not change. Spree now gives the Hungarian forint two decimal places, as ISO 4217 does.
803
- - **Money on the API is written to each currency's decimals.** Every amount in the Store, Admin and Seller APIs, and so in webhook payloads and email variables, is a decimal string with exactly its currency's decimal places: `"10.00"` (was `"10.0"`), `"1000"` in yen (was `"1000.0"`), `"1.500"` in dinar. Unit prices keep up to four decimals. Fields that were JSON numbers are now strings: `cart.order_minimum` and `order_minimum_shortfall`, the product filter price range `min`/`max`, report money metrics and percentages, and the store's `preferred_default_minimum_payout_amount` and `preferred_default_commission_tax_rate`. Clients comparing these strings, or parsing them as numbers, should switch to exact decimal arithmetic; the SDKs export `sumMoney`, `subtractMoney`, `multiplyMoney`, `compareMoney` and `isZeroMoney`.
830
+ - **Money on the API is written to each currency's decimals.** Every amount in the Store, Admin and Seller APIs, and so in webhook payloads and email variables, is a decimal string with exactly its currency's decimal places: `"10.00"` (was `"10.0"`), `"1000"` in yen (was `"1000.0"`), `"1.500"` in dinar. Unit prices keep up to four decimals. Fields that were JSON numbers are now strings: `cart.order_minimum` and `order_minimum_shortfall`, the product filter price range `min`/`max`, report money metrics and percentages, and the store's `default_minimum_payout_amount` and `default_commission_tax_rate`. Clients comparing these strings, or parsing them as numbers, should switch to exact decimal arithmetic; the SDKs export `sumMoney`, `subtractMoney`, `multiplyMoney`, `compareMoney` and `isZeroMoney`.
804
831
  - **Admin and Seller writes refuse JSON numbers for money.** Amounts and rates, including those inside `preferences`, must be canonical decimal strings (`"19.99"`). A number, a localized or grouped string, or more decimals than the field holds is answered with `422` and `invalid_money_format`, naming the field.
805
832
  - **Amounts may not carry more decimals than they can keep.** Now that the columns hold four decimals, the database no longer rounds away a fraction of a cent, so a payment, refund, store credit, gift card, fee or discount with more decimals than its currency has (`10.0049` dollars) fails validation with `too_many_decimals`. Unit prices and cost prices allow four, tax rates five decimals of the fraction (`rate_percent` `"7.125"`, not `"7.1255"`), and price list percentages three. Round in your own code before saving. A manual order discount's `value` and a commission rate's `value` are read as strictly as other money fields.
833
+ - **The Admin and Seller APIs no longer return `display_*` money fields.** Responses carry the amount (`"29.99"`) with its currency, and the client formats it in the reader's own language. Before 6.0 the server formatted a copy in the request's language, which could disagree with the editable amount beside it. Format with `Intl.NumberFormat` or your platform's equivalent. The Store API, webhook payloads and email templates keep their `display_*` fields, and labels such as `display_name` are unchanged.
834
+ - **Money preferences have their own type.** Calculator, rule and store preferences holding an amount (`amount`, `first_item`, `minimal_amount`, `amount_min`, `min_amount`, `default_minimum_payout_amount` and the like) are declared `:money` instead of `:decimal`, report `money` in `preference_schema`, and are written with the decimals of the calculator's currency (`"12.50"` rather than `"12.5"`). Declare your own extensions' amounts with `:money` so the dashboard shows their currency. A `:decimal` or `:money` preference now refuses text such as `"1,599.99"` instead of reading it as 1.
806
835
  - **Rates are decimal strings, and tax rates are renamed.** Rates and percentages read and write as strings without trailing zeros. A tax rate's `amount` (a fraction) is now `rate`, and `amount_percentage` is `rate_percent`; `Spree::TaxRate#amount_percentage` keeps working with a warning until 6.1.
807
836
  - **`amount_in_cents` and `compare_at_amount_in_cents` are removed** from prices and price history. Read `amount`, and each currency's `decimal_places` (new on `GET /api/v3/store/currencies`) if you need minor units.
808
837
  - **Money columns widen to `decimal(19,4)`.** They now hold a Kuwaiti dinar's third decimal, unit prices below a cent (`0.0125`), and very large amounts in currencies like the Vietnamese dong. No stored value changes. Totals, payments and refunds are still rounded to the currency's decimals; only unit prices (`price`, `compare_at_amount`, `cost_price`, `unit_cost`) keep up to four. The migration rewrites each money table while it runs, which locks `spree_orders`, `spree_line_items` and `spree_adjustments` for as long as that takes. If you cannot take that lock, widen those columns online first (add a `decimal(19,4)` shadow column, backfill it in batches, swap it in), and the migration skips any column already at `(19,4)`. Extensions with their own money tables, such as `spree_stripe_payment_intents` from the standalone Stripe gem, should widen theirs the same way.
@@ -849,6 +878,9 @@ Before 6.0 a cart was an incomplete order, so extensions written for 5.x (paymen
849
878
  | `Address#firstname`, `#lastname`, `#zipcode` | `#first_name`, `#last_name`, `#postal_code` (columns renamed) |
850
879
  | `Address.normalize_zipcode`, `#normalized_zipcode` | `.normalize_postal_code`, `#normalized_postal_code` |
851
880
  | `Order#bill_address_firstname`, `#bill_address_lastname` | `#bill_address_first_name`, `#bill_address_last_name` |
881
+ | `preference … parse_on_set: normalize_id_preference(klass:, scope:)` | `preference …, :array, of: :id, model:, scope:` |
882
+ | `preference …, in: [...]` | `preference …, choices: [...]` |
883
+ | `:array` without `of:`, `:hash` without `keys:`/`values:`, the `:any` type | A complete declaration (see [Model preferences](../customization/model-preferences.md#declaring-the-full-type)) |
852
884
  | `OptionType#presentation`, `OptionValue#presentation` | `#label` (column renamed, translations included). Reads and writes on an instance only — `where(presentation:)` and `find_by(presentation:)` raise, because Mobility owns `label` so the bridge cannot be an attribute alias. Query by `label`. |
853
885
  | `OptionValue#option_type_presentation` | `#option_type_label` |
854
886
  | `q[presentation_cont]` (option type / value filters) | `q[label_cont]` — the Ransack whitelist publishes the column, so the filter key moves with it |
@@ -24,9 +24,9 @@ const { data: providers } = await adminClient.payoutProviders.list()
24
24
  // find the row with id 'stripe' — `available` says whether this store can use it
25
25
 
26
26
  await adminClient.store.update({
27
- preferred_payout_provider: 'stripe',
28
- preferred_default_payouts_schedule_interval: 'weekly',
29
- preferred_default_minimum_payout_amount: 50,
27
+ payout_provider: 'stripe',
28
+ default_payouts_schedule_interval: 'weekly',
29
+ default_minimum_payout_amount: '50.00',
30
30
  })
31
31
  ```
32
32
 
@@ -34,7 +34,7 @@ await adminClient.store.update({
34
34
  curl -X PATCH https://your-store.com/api/v3/admin/store \
35
35
  -H "X-Spree-API-Key: sk_xxx" \
36
36
  -H "Content-Type: application/json" \
37
- -d '{"preferred_payout_provider": "stripe", "preferred_default_payouts_schedule_interval": "weekly", "preferred_default_minimum_payout_amount": 50}'
37
+ -d '{"payout_provider": "stripe", "default_payouts_schedule_interval": "weekly", "default_minimum_payout_amount": "50.00"}'
38
38
  ```
39
39
 
40
40
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spree/docs",
3
- "version": "0.1.304",
3
+ "version": "0.1.306",
4
4
  "description": "Spree Commerce developer documentation for AI agents and local reference",
5
5
  "type": "module",
6
6
  "license": "CC-BY-4.0",