@spree/docs 0.1.177 → 0.1.178

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 (117) hide show
  1. package/dist/developer/cli/quickstart.md +1 -1
  2. package/dist/developer/contributing/creating-an-extension.md +2 -2
  3. package/dist/developer/core-concepts/addresses.md +3 -3
  4. package/dist/developer/core-concepts/architecture.md +4 -4
  5. package/dist/developer/core-concepts/calculators.md +2 -2
  6. package/dist/developer/core-concepts/carts.md +207 -0
  7. package/dist/developer/core-concepts/channels.md +3 -3
  8. package/dist/developer/core-concepts/customers.md +1 -1
  9. package/dist/developer/core-concepts/events.md +1 -1
  10. package/dist/developer/core-concepts/fulfillments.md +278 -0
  11. package/dist/developer/core-concepts/imports-exports.md +6 -18
  12. package/dist/developer/core-concepts/inventory.md +3 -3
  13. package/dist/developer/core-concepts/media.md +1 -1
  14. package/dist/developer/core-concepts/metafields.md +1 -1
  15. package/dist/developer/core-concepts/orders.md +92 -339
  16. package/dist/developer/core-concepts/payments.md +1 -1
  17. package/dist/developer/core-concepts/promotions.md +68 -195
  18. package/dist/developer/core-concepts/returns-exchanges-claims.md +173 -0
  19. package/dist/developer/core-concepts/store-credits-gift-cards.md +16 -18
  20. package/dist/developer/core-concepts/stores.md +1 -1
  21. package/dist/developer/core-concepts/taxes-discounts-fees.md +199 -0
  22. package/dist/developer/core-concepts/taxes.md +3 -3
  23. package/dist/developer/create-spree-app/quickstart.md +1 -1
  24. package/dist/developer/customization/api.md +36 -11
  25. package/dist/developer/customization/configuration.md +207 -37
  26. package/dist/developer/customization/decorators.md +8 -8
  27. package/dist/developer/customization/permissions.md +34 -229
  28. package/dist/developer/customization/quickstart.md +159 -108
  29. package/dist/developer/customization/validations.md +1 -1
  30. package/dist/developer/dashboard/overview.md +1 -1
  31. package/dist/developer/how-to/build-a-b2b-store.md +21 -0
  32. package/dist/developer/how-to/build-a-marketplace.md +25 -0
  33. package/dist/developer/how-to/custom-api-authentication.md +1 -1
  34. package/dist/developer/how-to/custom-delivery-rate-provider.md +242 -0
  35. package/dist/developer/how-to/custom-document-numbers.md +154 -0
  36. package/dist/developer/how-to/custom-order-routing.md +1 -1
  37. package/dist/developer/how-to/custom-payment-method.md +1 -1
  38. package/dist/developer/how-to/custom-promotion.md +76 -115
  39. package/dist/developer/how-to/custom-report.md +2 -2
  40. package/dist/developer/how-to/custom-stock-splitter.md +5 -5
  41. package/dist/developer/how-to/sell-digital-products.md +20 -0
  42. package/dist/developer/multi-tenant/quickstart.md +1 -1
  43. package/dist/developer/providers/dam.md +14 -0
  44. package/dist/developer/providers/erp.md +31 -0
  45. package/dist/developer/providers/fulfillment.md +25 -0
  46. package/dist/developer/{deployment/telemetry.md → providers/observability.md} +1 -1
  47. package/dist/developer/providers/overview.md +44 -0
  48. package/dist/developer/providers/pim.md +25 -0
  49. package/dist/developer/providers/sso.md +20 -0
  50. package/dist/developer/sdk/admin/extending.md +1 -1
  51. package/dist/developer/sdk/admin/quickstart.md +1 -1
  52. package/dist/developer/sdk/admin/resources.md +1 -1
  53. package/dist/developer/sdk/extending.md +1 -1
  54. package/dist/developer/tutorial/admin-api.md +13 -0
  55. package/dist/developer/tutorial/dashboard-plugin.md +14 -0
  56. package/dist/developer/tutorial/events.md +7 -161
  57. package/dist/developer/tutorial/introduction.md +15 -35
  58. package/dist/developer/tutorial/model.md +7 -98
  59. package/dist/developer/tutorial/store-api.md +13 -0
  60. package/dist/developer/tutorial/storefront.md +12 -0
  61. package/dist/developer/tutorial/testing.md +7 -711
  62. package/dist/developer/upgrades/5.6-to-6.0.md +2 -2
  63. package/package.json +1 -1
  64. package/dist/developer/admin/admin.md +0 -214
  65. package/dist/developer/admin/authentication.md +0 -59
  66. package/dist/developer/admin/components.md +0 -711
  67. package/dist/developer/admin/custom-css.md +0 -256
  68. package/dist/developer/admin/custom-javascript.md +0 -116
  69. package/dist/developer/admin/extending-ui.md +0 -1839
  70. package/dist/developer/admin/form-builder.md +0 -444
  71. package/dist/developer/admin/helper-methods.md +0 -531
  72. package/dist/developer/admin/navigation.md +0 -805
  73. package/dist/developer/admin/tables.md +0 -490
  74. package/dist/developer/advanced/adding_spree_to_rails_app.md +0 -92
  75. package/dist/developer/core-concepts/adjustments.md +0 -113
  76. package/dist/developer/core-concepts/reports.md +0 -208
  77. package/dist/developer/core-concepts/shipments.md +0 -307
  78. package/dist/developer/core-concepts/users.md +0 -303
  79. package/dist/developer/customization/authentication.md +0 -100
  80. package/dist/developer/customization/checkout.md +0 -202
  81. package/dist/developer/customization/emails.md +0 -18
  82. package/dist/developer/customization/routes.md +0 -24
  83. package/dist/developer/multi-vendor/installation.md +0 -61
  84. package/dist/developer/multi-vendor/quickstart.md +0 -17
  85. package/dist/developer/tutorial/admin.md +0 -206
  86. package/dist/developer/tutorial/api.md +0 -606
  87. package/dist/developer/tutorial/extending-models.md +0 -393
  88. package/dist/developer/tutorial/sdk.md +0 -170
  89. package/dist/developer/upgrades/2.0-to-2.1.md +0 -46
  90. package/dist/developer/upgrades/2.1-to-2.2.md +0 -59
  91. package/dist/developer/upgrades/2.2-to-2.3.md +0 -44
  92. package/dist/developer/upgrades/2.3-to-2.4.md +0 -42
  93. package/dist/developer/upgrades/3.0-to-3.1.md +0 -47
  94. package/dist/developer/upgrades/3.1-to-3.2.md +0 -34
  95. package/dist/developer/upgrades/3.2-to-3.3.md +0 -70
  96. package/dist/developer/upgrades/3.3-to-3.4.md +0 -36
  97. package/dist/developer/upgrades/3.4-to-3.5.md +0 -44
  98. package/dist/developer/upgrades/3.5-to-3.6.md +0 -40
  99. package/dist/developer/upgrades/3.6-to-3.7.md +0 -62
  100. package/dist/developer/upgrades/3.7-to-4.0.md +0 -152
  101. package/dist/developer/upgrades/4.0-to-4.1.md +0 -92
  102. package/dist/developer/upgrades/4.1-to-4.2.md +0 -109
  103. package/dist/developer/upgrades/4.10-to-5.0.md +0 -131
  104. package/dist/developer/upgrades/4.2-to-4.3.md +0 -100
  105. package/dist/developer/upgrades/4.3-to-4.4.md +0 -125
  106. package/dist/developer/upgrades/4.4-to-4.5.md +0 -94
  107. package/dist/developer/upgrades/4.5-to-4.6.md +0 -119
  108. package/dist/developer/upgrades/4.6-to-4.7.md +0 -39
  109. package/dist/developer/upgrades/4.8-to-4.9.md +0 -24
  110. package/dist/developer/upgrades/4.9-to-4.10.md +0 -24
  111. package/dist/developer/upgrades/4.x-to-4.8.md +0 -52
  112. package/dist/developer/upgrades/5.0-to-5.1.md +0 -28
  113. package/dist/developer/upgrades/5.1-to-5.2.md +0 -131
  114. package/dist/developer/upgrades/5.2-to-5.3.md +0 -338
  115. package/dist/developer/upgrades/5.3-to-5.4.md +0 -277
  116. package/dist/developer/upgrades/5.4-to-5.5.md +0 -301
  117. package/dist/developer/upgrades/5.5-to-5.6.md +0 -207
@@ -0,0 +1,242 @@
1
+ ---
2
+ title: Build a Custom Delivery Rate Provider
3
+ description: Connect a carrier or rate aggregator to Spree so checkout quotes live delivery rates instead of calculator-based prices.
4
+ ---
5
+
6
+ ## Overview
7
+
8
+ A delivery rate provider decides **where a delivery method's price comes from**. Spree ships with one built-in provider, `Spree::DeliveryRateProvider::Internal`, which prices through the method's calculator — flat rates, per-item rates, and the rest. That is the default, and stores that never install a carrier integration keep using it.
9
+
10
+ A custom provider replaces that arithmetic with a live quote: you call a carrier or an aggregator, and the rate that reaches checkout carries the carrier name, the service level, and an estimated delivery date.
11
+
12
+ Use a provider when the price comes from outside Spree. If you only need different arithmetic on values Spree already has, write a calculator instead — it is far less work. See [Fulfillments](../core-concepts/fulfillments.md) for how delivery methods, rates and fulfillments fit together.
13
+
14
+ ## What you will build
15
+
16
+ Three pieces, each with one job:
17
+
18
+ | Piece | Responsibility |
19
+ |---|---|
20
+ | `Spree::Integration` subclass | Holds the store's credentials, managed from the admin dashboard |
21
+ | `Spree::DeliveryRateProvider::Base` subclass | Turns a package into a rate quote |
22
+ | An initializer | Registers both so admins can select them |
23
+
24
+ Providers are stateless strategy objects. They never store credentials themselves — that is the integration's job, which is what gives merchants one place to see everything connected to their store.
25
+
26
+ ## 1. Store credentials in an integration
27
+
28
+ ```ruby
29
+ module SpreeAcmeCarrier
30
+ class Integration < Spree::Integration
31
+ preference :api_key, :password
32
+ preference :account_number, :string
33
+ preference :test_mode, :boolean, default: true
34
+
35
+ def self.integration_group = 'shipping'
36
+
37
+ # Called when an admin activates the integration. Returning false blocks
38
+ # activation and shows your message in the dashboard.
39
+ def can_connect?
40
+ client.ping
41
+ true
42
+ rescue StandardError => e
43
+ self.connection_error_message = e.message
44
+ false
45
+ end
46
+
47
+ def client
48
+ @client ||= AcmeCarrier::Client.new(api_key: preferred_api_key, test: preferred_test_mode)
49
+ end
50
+ end
51
+ end
52
+ ```
53
+
54
+ Declare secrets as `:password` preferences. Spree masks them on read and guards the round-trip on write, so an API key never leaves the server in plain text.
55
+
56
+ ## 2. Implement the provider
57
+
58
+ Implement `#estimates` — it receives a `Spree::Stock::Package` and returns an array of `Estimate` objects, one per carrier service you can quote. Every estimate becomes its own named option at checkout ("Acme Ground", "Acme Express"), and the merchant narrows, renames, or marks up individual services from the delivery method's carrier-services card. An empty array hides the method.
59
+
60
+ (Single-quote providers can implement `#estimate` returning one `Estimate` or `nil` instead — the base class wraps it.)
61
+
62
+ ```ruby
63
+ module SpreeAcmeCarrier
64
+ class DeliveryRateProvider < Spree::DeliveryRateProvider::Base
65
+ def self.integration_class = 'SpreeAcmeCarrier::Integration'
66
+
67
+ def estimates(package)
68
+ quotes_for(package).map do |quote|
69
+ Spree::DeliveryRateProvider::Estimate.new(
70
+ cost: quote.amount,
71
+ # ISO code of the currency the carrier quoted in. Estimates in a
72
+ # different currency than the cart's are dropped — never convert
73
+ # silently; nil means the store's own currency.
74
+ currency: quote.currency,
75
+ carrier: quote.carrier_name,
76
+ service_level: quote.service_name,
77
+ estimated_delivery_date: quote.delivery_date,
78
+ metadata: { quote_id: quote.id }
79
+ )
80
+ end
81
+ rescue AcmeCarrier::Error => e
82
+ # Never raise into checkout: report and hide this method instead.
83
+ Rails.error.report(e, context: { delivery_method_id: delivery_method.id }, source: 'acme.rating')
84
+ []
85
+ end
86
+
87
+ private
88
+
89
+ # One API call serves every delivery method sharing this provider within
90
+ # a request — carriers return all services in one response, so quoting
91
+ # five methods should not mean five round-trips.
92
+ def quotes_for(package)
93
+ key = [:acme_quotes, store.id, package.stock_location.id, package.order.id]
94
+ Spree::Current.provider_cache[key] ||= integration.client.rates(
95
+ from: package.stock_location,
96
+ to: package.order.ship_address,
97
+ parcel: { weight: package.weight }
98
+ )
99
+ end
100
+ end
101
+ end
102
+ ```
103
+
104
+ **Never let `estimates` raise.** It runs inside checkout's rate refresh, so an exception breaks the whole delivery step for the customer. Rescue your carrier's errors, report them (`Rails.error.report`), and return `[]` — the method disappears from the options while everything else keeps working.
105
+
106
+ Optionally implement `def self.service_catalog(integration)` so the admin service picker can list the carrier's services as checkboxes. Fetch them live — a hardcoded list offers services the merchant has not enabled and hides the ones they have. Return one of three shapes, and never raise:
107
+
108
+ ```ruby
109
+ def self.service_catalog(integration)
110
+ return Spree::DeliveryRateProvider::ServiceCatalog.none if integration.nil?
111
+
112
+ services = integration.client.services.map do |service|
113
+ { carrier: service.carrier, service: service.code, label: service.name }
114
+ end
115
+ Spree::DeliveryRateProvider::ServiceCatalog.listing(services)
116
+ rescue AcmeCarrier::Error => e
117
+ # The merchant sees the carrier's own words — "no services" and "we
118
+ # could not ask" are different problems.
119
+ Spree::DeliveryRateProvider::ServiceCatalog.unavailable(e.message)
120
+ end
121
+ ```
122
+
123
+ `ServiceCatalog.none` is the default: the provider lists nothing and the merchant types identifiers free-form, which also stays available when a listing fails. Carrier and service values must match what your rates carry — the picker's rows are matched against quoted rates by exactly those two fields.
124
+
125
+ **Returning `nil` hides the delivery method** for that package. This is the same contract calculators follow, and it is how you express "this carrier does not serve this destination" — the method simply does not appear at checkout rather than appearing at a wrong price.
126
+
127
+ `cost` is pre-tax and pre-VAT. Spree applies the gross-up and resolves the tax rate afterwards, exactly as it does for calculator output, so you never handle tax yourself.
128
+
129
+ Declaring `integration_class` is all the availability wiring you need: Spree derives `available_for_store?` from it, so your provider is hidden from the admin picker — and rejected on save — until the merchant connects the integration.
130
+
131
+ ### Optional lifecycle hooks
132
+
133
+ `book` and `release` are part of the provider contract but Spree does not invoke them yet — they are reserved for the rate booking flow. Until then, call `book` yourself from your gem's fulfillment provider when the label is purchased:
134
+
135
+ ```ruby
136
+ # Reserve the quote once your fulfillment provider dispatches.
137
+ def book(delivery_rate)
138
+ integration.client.book(delivery_rate.metadata['quote_id'])
139
+ end
140
+
141
+ # Release a quote that is no longer wanted.
142
+ def release(delivery_rate)
143
+ integration.client.release(delivery_rate.metadata['quote_id'])
144
+ end
145
+ ```
146
+
147
+ ## 3. Register both classes
148
+
149
+ ```ruby
150
+ # config/initializers/spree.rb
151
+ Spree.delivery_rate_providers << 'SpreeAcmeCarrier::DeliveryRateProvider'.constantize
152
+ Spree.integrations << 'SpreeAcmeCarrier::Integration'
153
+ ```
154
+
155
+ Registration is what makes a provider selectable: Spree validates `rate_provider` against this list, so a typo fails when the method is saved rather than deep inside checkout.
156
+
157
+ ## 4. Use it
158
+
159
+ A merchant connects the integration under **Settings → Integrations**, then picks the provider on a delivery method under **Settings → Delivery methods**. The provider field only appears once more than one provider is available, so stores without a carrier integration never see it.
160
+
161
+ One delivery method is the carrier connection: every service your provider returns becomes its own option at checkout, so a single "Acme shipping" method is usually all a merchant creates. The method's carrier-services card narrows which services are offered, renames them ("Acme 1 day"), and adds per-service or method-wide markup.
162
+
163
+ Providers and calculators coexist freely. A store can price "Free shipping" with a flat-rate calculator, "Express" through your carrier, and "Local pickup" with neither.
164
+
165
+ ## Feeding carrier tracking back in
166
+
167
+ Once a parcel is moving, most carriers will tell you where it is. Spree keeps
168
+ that on a separate field from the fulfillment's own status, so a bounced parcel
169
+ never un-ships itself — see [Fulfillments](../core-concepts/fulfillments.md#statuses).
170
+
171
+ Spree owns the endpoint: every integration gets one at
172
+ `POST /api/v3/webhooks/fulfillments/:integration_id` — the merchant pastes
173
+ that URL into the carrier's webhook settings. Your job is one method on the
174
+ integration:
175
+
176
+ ```ruby
177
+ class SpreeAcmeCarrier::Integration < Spree::Integration
178
+ preference :api_key, :password
179
+ preference :webhook_secret, :password
180
+
181
+ # Verify the signature against your secret, translate the payload, return
182
+ # UpdateTracking arguments plus the tracking code to match on — or nil for
183
+ # events you don't act on.
184
+ def parse_webhook_event(raw_post, headers)
185
+ raise Spree::Integration::WebhookSignatureError if preferred_webhook_secret.blank?
186
+
187
+ payload = AcmeCarrier.verify!(raw_post, headers, preferred_webhook_secret) # raises on a bad signature
188
+
189
+ return unless payload['type'] == 'shipment.status_changed'
190
+
191
+ {
192
+ tracking_code: payload['tracking_code'],
193
+ tracking_status: STATUS_MAP.fetch(payload['status'], 'unknown'), # Spree::Fulfillment::TRACKING_STATUSES
194
+ estimated_delivery_at: payload['eta'],
195
+ delivered_at: payload['delivered_scan_at'],
196
+ details: payload.slice('status_detail', 'carrier')
197
+ }
198
+ rescue AcmeCarrier::SignatureError => e
199
+ raise Spree::Integration::WebhookSignatureError, e.message
200
+ end
201
+ end
202
+ ```
203
+
204
+ The endpoint does the rest: signature failures answer `401` so a misconfigured
205
+ sender notices, unmatched tracking codes and `nil` events are acknowledged with
206
+ `200` so the carrier never retries a payload that cannot succeed, and matched
207
+ events run through `Fulfillments::UpdateTracking` — reporting `delivered` also
208
+ confirms receipt on the fulfillment, and an update carrying only a scan leaves
209
+ an earlier estimate alone.
210
+
211
+ Three rules worth keeping:
212
+
213
+ - **Verify or refuse.** Raise `WebhookSignatureError` when no secret is
214
+ configured rather than accepting unsigned reports — a forged `delivered`
215
+ starts the customer's return window and, in the EU, the withdrawal clock.
216
+ - **Pass `delivered_at` from the carrier's delivering scan**, not the webhook's
217
+ arrival time. The webhook usually lands well after the parcel does.
218
+ - **Translate in your gem, never in core.** Map unrecognised carrier statuses
219
+ to `unknown` so the report stays visible instead of being dropped.
220
+
221
+ ## Testing
222
+
223
+ Your provider is a plain object, so test it directly:
224
+
225
+ ```ruby
226
+ RSpec.describe SpreeAcmeCarrier::DeliveryRateProvider do
227
+ let(:delivery_method) { create(:delivery_method, rate_provider: described_class.to_s) }
228
+
229
+ it 'quotes every service the carrier returns' do
230
+ estimates = described_class.new(delivery_method).estimates(package)
231
+
232
+ expect(estimates.map(&:service_level)).to contain_exactly('Ground', 'Express')
233
+ expect(estimates.first.carrier).to eq('Acme')
234
+ end
235
+
236
+ it 'hides the method when the carrier does not serve the destination' do
237
+ expect(described_class.new(delivery_method).estimates(unservable_package)).to eq([])
238
+ end
239
+ end
240
+ ```
241
+
242
+ Stub the carrier client rather than calling the real API — quoting runs on every checkout, so a slow or flaky test here is a slow or flaky suite everywhere.
@@ -0,0 +1,154 @@
1
+ ---
2
+ title: Customize Document Numbers
3
+ description: Change the shape of order, return and other document numbers — from the dashboard for order numbers, or in code for full control.
4
+ ---
5
+
6
+ ## Overview
7
+
8
+ Every order, return, exchange, claim, stock transfer, import and export carries a **document number** — the short, human-readable reference a merchant reads out on a support call and a customer quotes in an email. Orders look like `R1001`, returns like `RET1001`.
9
+
10
+ Numbers are separate from IDs. Every record also has a prefixed ID (`order_86Rf07xd4z`) which is what the API uses and what you should reference in code. The number exists purely so people can read, say and type it.
11
+
12
+ There are two ways to change it. Merchants can reshape **order numbers** from the dashboard without any code. Developers can replace the generator for **any** document type when the settings are not enough.
13
+
14
+ ## Merchant settings
15
+
16
+ **Settings → Store → Order numbers** controls four things:
17
+
18
+ | Setting | Effect |
19
+ |---|---|
20
+ | Numbering format | Sequential (default) or random. Applies to every numbered document. |
21
+ | Prefix | Leads each order number. Defaults to `R`. |
22
+ | Suffix | Ends each order number. Empty by default. |
23
+ | Start numbering at | The first order number. Defaults to `1001`. |
24
+
25
+ Sequential numbering counts up: `R1001`, `R1002`, `R1003`. Random numbering produces nine unpredictable digits, which is what Spree did before 6.0. The trade-off is worth stating to merchants: sequential numbers are far easier to read back over the phone, but a customer who orders twice can see how many orders you took in between. Random numbers give that away nothing, at the cost of legibility.
26
+
27
+ Two rules apply to every change:
28
+
29
+ - **Changes affect future numbers only.** Numbers already issued are permanent — they are printed on invoices, quoted in emails, and stored at payment gateways.
30
+ - **The starting value only applies before the first order.** Set it to `10001` on a new store and the first order is `R10001`, the second `R10002`. Once the counter has issued a number the field is locked in the dashboard, because raising it then would do nothing — the counter keeps going from where it is.
31
+
32
+ Prefix and suffix have no such restriction. Change them whenever you like: `INV` with suffix `-EU` and a start of `5001` gives `INV5001-EU`, and existing orders keep the numbers they were issued.
33
+
34
+ > **NOTE:** Sequential numbering is *mostly* gapless, not guaranteed gapless — an abandoned checkout or a rare collision consumes a value. Do not rely on it for legal invoice numbering, which in most jurisdictions requires a provably unbroken sequence.
35
+
36
+ ## Custom generators
37
+
38
+ When the settings are not enough — you need the year in the number, a per-warehouse prefix, or a format your ERP already expects — register a generator class.
39
+
40
+ A generator answers one question: what should this record's number be? Uniqueness is handled for you, so a generator only has to propose.
41
+
42
+ ```ruby
43
+ # lib/my_app/branch_order_numbers.rb
44
+ module MyApp
45
+ class BranchOrderNumbers < Spree::NumberGenerators::Base
46
+ # @param record [Spree::Order]
47
+ # @return [String]
48
+ def generate(record)
49
+ branch = record.number_store.code.upcase
50
+ year = Time.current.year
51
+
52
+ "#{branch}-#{year}-#{SecureRandom.random_number(10_000).to_s.rjust(4, '0')}"
53
+ end
54
+ end
55
+ end
56
+ ```
57
+
58
+ Register it in an initializer:
59
+
60
+ ```ruby
61
+ # config/initializers/spree.rb
62
+ Spree.number_generators[:order] = 'MyApp::BranchOrderNumbers'
63
+ ```
64
+
65
+ That is the whole contract. Orders now get numbers like `NYC-2026-0042`; every other document type keeps following the store's settings.
66
+
67
+ The resource key matches the model name, underscored: `:order`, `:return`, `:exchange`, `:claim`, `:stock_transfer`, `:import`, `:export`. Register the same class under several keys to share one format across document types.
68
+
69
+ A registered generator wins over the store's format setting — that is the point of registering one. If you want merchants to keep some control, read their settings yourself through `prefix_for` and `suffix_for` (below).
70
+
71
+ `Spree.number_generators.delete(:order)` removes the registration and hands numbering back to the store settings.
72
+
73
+ ### What a generator can read
74
+
75
+ The record is passed to `generate`, so anything reachable from it is available:
76
+
77
+ - `record.number_store` — the store whose settings apply, wherever it lives on the model
78
+ - `record.class.number_prefix` — the model's built-in prefix (`R`, `RET`, …)
79
+ - Any attribute of the record itself
80
+
81
+ Inheriting from `Spree::NumberGenerators::Base` also gives you `prefix_for(record)` and `suffix_for(record)`, which return the merchant's configured values for orders and the code-level prefix for everything else. Use them when you want to extend the merchant's choice rather than override it.
82
+
83
+ ### Sequential counters
84
+
85
+ If your generator needs its own counter, use the same one the built-in sequential generator uses rather than deriving a maximum from existing rows. This generator combines both — the merchant's prefix, a year, and a zero-padded counter:
86
+
87
+ ```ruby
88
+ module MyApp
89
+ class YearScopedNumbers < Spree::NumberGenerators::Base
90
+ def generate(record)
91
+ sequence = Spree::NumberSequence.next_value(
92
+ store: record.number_store,
93
+ resource_type: 'order',
94
+ start_at: 1
95
+ )
96
+
97
+ "#{prefix_for(record)}-#{Time.current.year}-#{sequence.to_s.rjust(5, '0')}"
98
+ end
99
+ end
100
+ end
101
+ ```
102
+
103
+ With the default prefix that produces `R-2026-00001`; a merchant who changes their prefix to `ACME` gets `ACME-2026-00001` without you touching the class.
104
+
105
+ Deriving the next value by parsing existing numbers looks simpler but breaks in three ways: legacy numbers from before your format existed do not parse, string columns sort `R999` above `R1000`, and two concurrent checkouts read the same maximum and produce the same number. The counter is locked for the increment, so it hands out distinct values under load.
106
+
107
+ ## Adding numbers to your own model
108
+
109
+ Models you add can carry document numbers too:
110
+
111
+ ```ruby
112
+ class MyApp::Consignment < Spree.base_class
113
+ has_spree_number prefix: 'CN'
114
+ end
115
+ ```
116
+
117
+ The record is numbered before validation on create, and the number follows the store's format setting like everything else. The model needs a `number` column with a unique index:
118
+
119
+ ```ruby
120
+ add_column :my_app_consignments, :number, :string, null: false
121
+ add_index :my_app_consignments, :number, unique: true
122
+ ```
123
+
124
+ If the model does not respond to `store`, define `number_store` so the generator knows whose settings to read:
125
+
126
+ ```ruby
127
+ def number_store
128
+ warehouse.store
129
+ end
130
+ ```
131
+
132
+ ## Fulfillments and payments
133
+
134
+ Fulfillments and payments do **not** have their own numbers. Theirs are derived from the order they belong to — `R1001-F1` for the first parcel, `R1001-P1` for the first payment — so they group visibly with their order and follow whatever format the merchant chose.
135
+
136
+ This is not configurable and there is no generator to register. If you need a different shape, override `number` on the model.
137
+
138
+ ## Upgrading from Spree 5.x
139
+
140
+ Two changes matter.
141
+
142
+ **Numbers are sequential by default.** Stores upgrading from 5.x keep every existing number untouched, but new documents get sequential numbers starting at 1001. To keep the old behavior, set the store's numbering format to **Random** in Settings → Store → Order numbers.
143
+
144
+ **`Spree::Core::NumberGenerator` is deprecated.** Models using it still work through Spree 6.0 and log a deprecation warning. Replace it with the concern:
145
+
146
+ ```ruby
147
+ # before
148
+ include Spree::Core::NumberGenerator.new(prefix: 'R', length: 9)
149
+
150
+ # after
151
+ has_spree_number prefix: 'R'
152
+ ```
153
+
154
+ The `length:` and `letters:` options are gone — they described the random format, which is now the generator's business rather than the model's. If you were relying on a specific length, register a custom generator.
@@ -15,7 +15,7 @@ Order routing decides which [Stock Location](../core-concepts/inventory.md#stock
15
15
 
16
16
  This guide covers both. Most extensions are rules — they compose with the built-ins and don't require rewriting the pipeline.
17
17
 
18
- Before starting, make sure you understand [how order routing works in Spree](../core-concepts/shipments.md#order-routing).
18
+ Before starting, make sure you understand [how order routing works in Spree](../core-concepts/fulfillments.md#order-routing).
19
19
 
20
20
  | If the answer is "yes" | Pick |
21
21
  |---|---|
@@ -368,7 +368,7 @@ For every gateway action (authorize, purchase, capture, void, credit), Spree pas
368
368
  ## Related Documentation
369
369
 
370
370
  - [Payments](../core-concepts/payments.md) - Payment architecture and core concepts
371
- - [Checkout Customization](../customization/checkout.md) - Customizing the checkout flow
371
+ - [Checkout Customization](../core-concepts/carts.md) - Customizing the checkout flow
372
372
  - [Events](../core-concepts/events.md) - Subscribe to payment events
373
373
  - [Stripe Integration](../../integrations/payments/stripe.md) - Reference implementation using Stripe
374
374
  - [Adyen Integration](../../integrations/payments/adyen.md) - Reference implementation using Adyen