@spree/docs 0.1.141 → 0.1.142
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.
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Upgrading to Spree 6.0
|
|
3
|
+
description: Guide to upgrading a Spree 5.6 application to Spree 6.0
|
|
4
|
+
hidden: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
> **INFO:** Before proceeding to upgrade, please ensure you're at [Spree 5.6](5.5-to-5.6.md). Spree 6.0 requires **Rails 8.1** and is the designated breaking-change window for the platform — read the behavioral changes section even if your upgrade runs clean.
|
|
8
|
+
|
|
9
|
+
Spree 6.0 is a major release. The headline changes:
|
|
10
|
+
|
|
11
|
+
- **Cart and Order are separate models.** `Spree::Cart` owns shopping and checkout; completing checkout copies the cart into an immutable `Spree::Order`. The order state machine is gone.
|
|
12
|
+
- **Checkout has no server-side state machine.** Steps are advisory metadata for your frontend; the backend enforces exactly one hard gate — completion.
|
|
13
|
+
- **Adjustments are typed rows.** The polymorphic `Spree::Adjustment` is replaced by `Spree::TaxLine`, `Spree::Discount` and `Spree::Fee`.
|
|
14
|
+
- **Master Variant** is gone, replaced by a `default_variant_id` foreign key on `Spree::Product`. There's no hidden/dummy Variant created now, the default variant is a real Variant with its own SKU, price, stock, etc.
|
|
15
|
+
- **Fulfillment vocabulary.** `Shipment` → `Fulfillment`, `ShippingMethod` → `DeliveryMethod`, `Zone` → `DeliveryZone`, with a pluggable `FulfillmentProvider` strategy.
|
|
16
|
+
- **Two-tier services.** Plain services in `app/services`, plus `Spree::Workflow` classes in `app/workflows` for the curated multi-step flows (completion, cancellation, recalculation) — with named steps, instrumentation and extension hooks.
|
|
17
|
+
|
|
18
|
+
The upgrade is completed in four steps:
|
|
19
|
+
|
|
20
|
+
1. **Update the Ruby gems**
|
|
21
|
+
2. **Run database migrations**
|
|
22
|
+
3. **Run data backfills** — convert your existing records into the new schema
|
|
23
|
+
4. **Review behavioral changes** — this release changes runtime behavior, not just schema
|
|
24
|
+
|
|
25
|
+
## How to upgrade
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
```bash Spree CLI (Docker)
|
|
29
|
+
spree upgrade
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```bash Without Spree CLI
|
|
33
|
+
# cd backend if you're in the monorepo root
|
|
34
|
+
bundle update
|
|
35
|
+
bundle exec rake spree:install:migrations && bin/rails db:migrate
|
|
36
|
+
bundle exec rake spree:upgrade
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
Skipping versions and re-running are both safe — `bundle exec rake spree:upgrade` figures out what still needs to happen and does nothing on data that's already migrated.
|
|
41
|
+
|
|
42
|
+
## What the upgrade does
|
|
43
|
+
|
|
44
|
+
Reference material — the data backfills `bundle exec rake spree:upgrade` executes. Every task is idempotent.
|
|
45
|
+
|
|
46
|
+
### Convert incomplete orders into carts
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
bundle exec rake spree:migrate_incomplete_orders_to_carts
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Every order that never completed and was never canceled becomes a `Spree::Cart` with the same token, so in-flight guest and customer checkouts survive the deploy. The cart re-owns the order's line items, fulfillments, payments, payment sessions, reservations and coupon codes; the hollow order row is deleted. Completed and canceled orders are untouched. Orders holding payment sessions convert last, so an interrupted run leaves the riskiest rows for the retry.
|
|
53
|
+
|
|
54
|
+
### Convert legacy adjustments into typed rows
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
bundle exec rake spree:migrate_adjustments_to_typed_rows
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Rebuilds `spree_adjustments` into `Spree::TaxLine`, `Spree::Discount` and `Spree::Fee` rows. Orders whose typed sums do not reconcile with the stored totals are left untouched and flagged (`private_metadata['typed_adjustments_frozen']`) for manual review instead of silently changing money.
|
|
61
|
+
|
|
62
|
+
### Backfill fulfillment and delivery naming
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
bundle exec rake spree:migrate_shipping_to_delivery
|
|
66
|
+
bundle exec rake spree:migrate_zones_to_delivery_zones
|
|
67
|
+
bundle exec rake spree:migrate_calculator_bounds_to_delivery_method_rules
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Moves stored strings, statuses and class names to the fulfillment/delivery vocabulary, converts delivery-referenced `Spree::Zone` records into `Spree::DeliveryZone` with typed members, and converts FlatRate calculator eligibility bounds into `Spree::DeliveryMethodRule` records.
|
|
71
|
+
|
|
72
|
+
### Remove master variants
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
bundle exec rake spree:remove_master_variant
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Products now carry a `default_variant_id` foreign key; `is_master` is gone from the models. (The physical column drop lands in 6.1.)
|
|
79
|
+
|
|
80
|
+
### Categories and collections
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
bundle exec rake spree:migrate_taxons_to_categories_and_collections
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Taxons become `Spree::Category` (hierarchy) and automatic taxons become `Spree::Collection` (flat, rule-based). `Spree::Taxon` remains as an alias for one release.
|
|
87
|
+
|
|
88
|
+
### Backfill order coupon codes
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
bundle exec rake spree:backfill_order_coupon_codes
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`spree_orders` gains a `coupon_code` column (parity with carts). Historical placed orders applied coupons only through the promotion join tables — this fills the column from the attached coupon-code record (or the applied single-code promotion) so admin filtering and the serializer answer consistently for old orders.
|
|
95
|
+
|
|
96
|
+
### Markets on orders
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
bundle exec rake spree:backfill_order_markets
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Orders missing a market get the store default — `market` is required on carts and orders in 6.0.
|
|
103
|
+
|
|
104
|
+
## The Cart/Order split
|
|
105
|
+
|
|
106
|
+
The single biggest change. What used to be one `Spree::Order` living through checkout and beyond is now two models:
|
|
107
|
+
|
|
108
|
+
- **`Spree::Cart`** (`spree_carts`, prefixed IDs `cart_...`) owns the shopping and checkout phase: line items, addresses, payments-in-progress, delivery proposals, promotions, stock reservations. Carts have **no status column** — `completed_at` is the only lifecycle marker.
|
|
109
|
+
- **`Spree::Order`** is created *by completion* — the cart is copied into it (line items, fulfillments, addresses, typed money rows — **copies, never shared rows**). Orders carry `status` (`draft` / `placed` / `canceled`) and are money-frozen once placed.
|
|
110
|
+
|
|
111
|
+
Consequences to review:
|
|
112
|
+
|
|
113
|
+
- **Completed carts are read-only.** The cart is retained after completion (abandonment analytics, idempotent replay) but rejects every write. Post-checkout life belongs to the order.
|
|
114
|
+
- **Completion is idempotent.** `Spree::Carts::Complete` guards with a unique `spree_orders.cart_id` index and a `completing_at` lock: a double-clicked Place Order returns the same order, a crashed completion replays safely, and a pre-capture payment failure rolls the draft order back and re-points payments to the cart.
|
|
115
|
+
- **The guest token carries over** from cart to order, so confirmation pages keep working with the credential the guest already holds.
|
|
116
|
+
- **Dual concrete FKs, not polymorphism.** Records owned by either side (`LineItem`, `Fulfillment`, `TaxLine`, `Discount`, `Fee`, `Payment`, `StockReservation`) carry nullable `cart_id` + `order_id` with an exactly-one rule and an `#owner` method. Code that assumed `line_item.order` is always present must read `line_item.owner`.
|
|
117
|
+
- **Shared model surface** lives in `Spree::Purchase::*` concerns (addresses, taxation, store credits, gift cards, digital items, payment processing, market/channel/currency/locale resolution) — included by both Cart and Order. Decorators targeting `Spree::Order` methods that moved should decorate the concern or the new owner.
|
|
118
|
+
|
|
119
|
+
## Checkout without a state machine
|
|
120
|
+
|
|
121
|
+
`Spree::Order` no longer has a `state` column, a state machine, or the `checkout_flow` DSL. If your app customized checkout with `checkout_flow`, `go_to_state`, `insert_checkout_step`, `remove_checkout_step` or `remove_transition` — those APIs are gone (not deprecated: the machine they configured no longer exists).
|
|
122
|
+
|
|
123
|
+
The replacement model:
|
|
124
|
+
|
|
125
|
+
- **Steps are advisory.** `cart.checkout_steps`, `current_checkout_step` and `completed_checkout_steps` are *derived* from cart data — there is no stored step and no server-side sequencing. Clients may write any checkout field in any order. "Steps" are a grouping label telling your frontend which page an unmet requirement belongs to.
|
|
126
|
+
- **One hard gate.** `Spree::Carts::Complete` is the only place checkout is enforced. It validates the full requirement battery (line items, email, addresses, delivery selection, payment coverage, per-item stock, discontinued products, guest policy) and returns structured `{ step, field, code, message }` errors.
|
|
127
|
+
- **`Spree::Checkout::Registry` is the extension surface.** One declaration serves both the advisory feed and the completion gate:
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
# config/initializers/spree.rb
|
|
131
|
+
Rails.application.config.to_prepare do
|
|
132
|
+
Spree::Checkout::Registry.add_requirement(
|
|
133
|
+
step: :payment,
|
|
134
|
+
field: :po_number,
|
|
135
|
+
message: 'PO number is required',
|
|
136
|
+
satisfied: ->(cart) { cart.metadata['po_number'].present? },
|
|
137
|
+
applicable: ->(cart) { cart.customer.present? }
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The requirement appears in the Cart API's `requirements` array (so a storefront rendering the feed generically needs zero changes) *and* blocks completion. `register_step` adds whole steps (spliced into `checkout_steps` at `before:`/`after:` anchors); built-in steps are customized through `Registry.base_steps` — an ordered `{ name => applicability }` hash you can mutate directly (`base_steps.delete('confirm')`).
|
|
143
|
+
- **The API `requirements` array now carries a stable `code`** on every entry (`email_required`, `out_of_stock`, `guest_checkout_not_allowed`, ...). Additive change — existing consumers keep working.
|
|
144
|
+
- **The delivery requirement is keyed `delivery_method`, not `shipping_method`.** Its entry is now `{ step: 'delivery', field: 'delivery_method', code: 'delivery_method_required' }`. A storefront that renders the feed generically needs no change; one that keys off the field or code to highlight a specific input must switch both tokens. The `Spree.t('checkout_requirements.shipping_method_required')` translation key was renamed to `checkout_requirements.delivery_method_required` — override it under the new key.
|
|
145
|
+
- **"Logic between steps" has no backend home by design.** Side effects hang off data writes (workflow hooks such as `Carts::Complete`'s `before_finalize` and `Carts::AddItem`'s `after_item_added`) and events (`cart.updated`, `order.placed`) — not step transitions.
|
|
146
|
+
|
|
147
|
+
## Statuses: derived, then persisted
|
|
148
|
+
|
|
149
|
+
`payment_state` and `shipment_state` machine columns are replaced by `payment_status` and `fulfillment_status` — stored, indexed, and recomputed from payment/refund/fulfillment records by a single writer, `Spree::Orders::UpdateStatuses`. The legacy names remain as read aliases for one release.
|
|
150
|
+
|
|
151
|
+
Behavior to review:
|
|
152
|
+
|
|
153
|
+
- Nothing else writes these columns. If your code assigned `order.payment_state = 'paid'`, replace it with the underlying records (payments/refunds) and let the recompute derive.
|
|
154
|
+
- The `payment_status` domain gains `overcharged` and `voided`; `fulfillment_status` includes `backorder`. Money comparisons are quantized to currency precision and refunds are netted before comparing.
|
|
155
|
+
- A completed payment updates **only the payment side of the ledger** (`payment_total` + statuses). It never re-sums item or adjustment money.
|
|
156
|
+
- Fulfillment is a fact: a fulfilled fulfillment is never downgraded by later payment-state changes.
|
|
157
|
+
|
|
158
|
+
## Recalculation on write
|
|
159
|
+
|
|
160
|
+
Transition-triggered recalculation is gone with the machine. Instead:
|
|
161
|
+
|
|
162
|
+
- **`Spree::Carts::RecalculateTotals`** is the single totals seam: money inputs, typed-row regeneration (promotions via the winner-only adjuster, tax via `Spree.tax_provider`), folding and one persist. It runs on the writes that matter — item changes, address/market changes (which also re-price items and rebuild delivery proposals) — not on step transitions.
|
|
163
|
+
- Promotion eligibility is evaluated against **current** totals in the same recalculation — a cart crossing a coupon threshold gets the discount on that recalculation, not the next one.
|
|
164
|
+
- **Completed orders are money-frozen.** Typed rows are never regenerated post-placement; recalculation only re-sums them. Post-placement money edits go through the explicit admin services (`Orders::Discounts::*`, `Orders::Fees::*`), which write rows and re-sum.
|
|
165
|
+
- `Spree::OrderUpdater` and `Spree::CartUpdater` remain as deprecated shells — every method warns and runs the full recalculation. Removed in 6.1.
|
|
166
|
+
|
|
167
|
+
## Completion, in one workflow
|
|
168
|
+
|
|
169
|
+
`Spree::Orders::Complete` is the one home for everything that happens when an order becomes placed — payment processing (when needed), fulfillment finalization, placement, coupon/gift-card redemption, digital auto-fulfillment, statuses, and the `order.placed` event. Checkout reaches it through `Carts::Complete`; admin/B2B draft completion calls it directly (`payment_pending: true` places without processing payments for invoice-later flows).
|
|
170
|
+
|
|
171
|
+
- **`Order#finalize!` is deprecated** (removed in 6.1) and delegates to the workflow. Behavioral change: finalizing an **already-completed order is now a no-op** — the workflow halts idempotently instead of re-running side effects.
|
|
172
|
+
- **Completion side effects moved out of the model.** Newsletter subscription, checkout account creation and risk assessment run in the synchronous `Spree::OrderPlacedSubscriber` on the `order.placed` event. Decorators that patched `finalize!` should become event subscribers or `before_finalize` hook handlers.
|
|
173
|
+
- **`order.placed` is the completion event.** `order.completed` still fires as a deprecated alias for one release (webhook consumers should migrate; wildcard subscribers can dedupe on the `deprecated_alias_of` metadata marker).
|
|
174
|
+
- `Order.register_update_hook` no longer runs during completion.
|
|
175
|
+
|
|
176
|
+
## Addresses
|
|
177
|
+
|
|
178
|
+
The full address surface is shared by Cart and Order through `Spree::Purchase::Addresses`, which means cart checkout regains behavior that 5.x orders had:
|
|
179
|
+
|
|
180
|
+
- Address writes **deduplicate** against the customer's address book and **promote** checkout addresses to the customer's defaults (quick-checkout wallet addresses excluded).
|
|
181
|
+
- `ship_address_id=` / `bill_address_id=` are **ownership-guarded**: an address not owned by the record's customer resolves to `nil`.
|
|
182
|
+
- A signed-in customer entering checkout gets blank address slots **auto-filled from their saved defaults**.
|
|
183
|
+
- **`use_billing` is deprecated** (removed in 6.1): the shipping address is canonical — use `use_shipping` to copy ship → bill.
|
|
184
|
+
|
|
185
|
+
## Dependency injection changes
|
|
186
|
+
|
|
187
|
+
6.0 introduces `*_workflow` keys for the flows that graduated to the workflow tier. The old `*_service` keys **stay settable and readable one release so applications don't crash at boot — but a legacy write is stashed, not applied**: a class written against the old service contract is not interchangeable with the workflow the new call sites consume. Reads return your stashed class (legacy code calling its own override keeps working), falling back to the workflow. Removed in 6.1.
|
|
188
|
+
|
|
189
|
+
| Legacy key (stash-only) | 6.0 key | Resolves to |
|
|
190
|
+
|---|---|---|
|
|
191
|
+
| `cart_add_item_service` | `cart_add_item_workflow` | `Spree::Carts::AddItem` |
|
|
192
|
+
| `cart_recalculate_service` | `cart_recalculate_workflow` | `Spree::Carts::Recalculate` |
|
|
193
|
+
| `carts_complete_service` | `carts_complete_workflow` | `Spree::Carts::Complete` |
|
|
194
|
+
| `order_cancel_service` | `order_cancel_workflow` | `Spree::Orders::Cancel` |
|
|
195
|
+
| `order_complete_service` | `order_complete_workflow` | `Spree::Orders::Complete` |
|
|
196
|
+
| `shipment_update_service` | `fulfillment_update_service` | `Spree::Fulfillments::Update` |
|
|
197
|
+
|
|
198
|
+
New seams with no legacy counterpart:
|
|
199
|
+
|
|
200
|
+
| Key | Resolves to | Purpose |
|
|
201
|
+
|---|---|---|
|
|
202
|
+
| `cart_recalculate_totals_workflow` | `Spree::Carts::RecalculateTotals` | the single totals seam |
|
|
203
|
+
| `order_recalculate_totals_workflow` | `Spree::Orders::RecalculateTotals` | order twin (post-placement re-sum) |
|
|
204
|
+
| `order_resume_workflow` | `Spree::Orders::Resume` | resume canceled orders |
|
|
205
|
+
| `order_discount_create_service` | `Spree::Orders::Discounts::Create` | renamed from `order_add_manual_discount_service` (never released) |
|
|
206
|
+
| `order_update_statuses_service` | `Spree::Orders::UpdateStatuses` | the sole status writer |
|
|
207
|
+
|
|
208
|
+
Removed keys (their classes no longer exist): `carts_validate_service` (completion validation is `Spree::Checkout::Requirements` directly), plus the dead legacy `Spree::Cart::*` service namespace registrations.
|
|
209
|
+
|
|
210
|
+
If you override a workflow seam, subclass the shipped workflow (or implement the same `perform` keyword contract) — workflow arguments are plain Ruby keywords, so a mismatch raises `ArgumentError` at call time, not silently.
|
|
211
|
+
|
|
212
|
+
## Deprecated in 6.0, removed in 6.1
|
|
213
|
+
|
|
214
|
+
Every rename keeps the legacy name working for one release with a deprecation warning. The notable ones:
|
|
215
|
+
|
|
216
|
+
| Deprecated | Use instead |
|
|
217
|
+
|---|---|
|
|
218
|
+
| `Order#finalize!` | `Spree.order_complete_workflow` |
|
|
219
|
+
| `Order#updater`, `Spree::OrderUpdater`, `Spree::CartUpdater` | `#recalculate_totals!` / `#update_statuses!` |
|
|
220
|
+
| `Order#shipping_discount` | `#fulfillment_discount` |
|
|
221
|
+
| `Order#special_instructions` | `#customer_note` (column renamed) |
|
|
222
|
+
| `Order#promo_total`, `#item_count`, `#ship_total` | `#discount_total`, `#total_quantity`, `#delivery_total` (columns renamed) |
|
|
223
|
+
| `use_billing` / `clone_billing_address` | `use_shipping` (shipping address is canonical) |
|
|
224
|
+
| `Fulfillment#ship`, `#ship!`, `#shipped?`, `#can_ship?`, `#shipping_method`, `#add_shipping_method` | `#fulfill`, `#fulfill!`, `#fulfilled?`, `#can_fulfill?`, `#delivery_method`, `#add_delivery_method` |
|
|
225
|
+
| `LineItem#target_shipment` | `#target_fulfillment` |
|
|
226
|
+
| `Order#create_proposed_shipments` / `#create_proposed_fulfillments` | `#rebuild_fulfillments!` (Cart ships with the new name only) |
|
|
227
|
+
| `Order#remove_out_of_stock_items!` | cart-side only (`Spree::Carts::RemoveOutOfStockItems`) |
|
|
228
|
+
| `Order#delivery_required?` | `#delivery_step_required?` — digital and pickup are deliveries too; this asks whether the customer must choose a delivery option |
|
|
229
|
+
| `Order#requires_ship_address?` | `#shipping_address_required?` — decided by the selected delivery methods (`DeliveryMethod#requires_address?`); digital, pickup, and pickup-point deliveries need no customer address |
|
|
230
|
+
| `requirements[].field` / `.code` `shipping_method` (Store API) | `delivery_method` / `delivery_method_required` — no bridge; clients keying off the delivery requirement must switch both tokens |
|
|
231
|
+
| `Cart#number` (Store API field) | `id` — carts have no order-style number; the field mirrors the prefixed ID for one release |
|
|
232
|
+
| `order.completed` event | `order.placed` |
|
|
233
|
+
| Calling cart services with `order:` kwargs | `cart:` kwargs |
|
|
234
|
+
| `OrderWalkthrough` (testing support) | factories: `:cart_ready_for_delivery`, `:cart_ready_to_complete`, `:completed_order_with_totals` |
|
|
235
|
+
|
|
236
|
+
## For extension authors
|
|
237
|
+
|
|
238
|
+
- **Don't reach for model business methods from services** — 6.0 code style writes behavior inline in service/workflow steps; models keep data, validations, predicates and persistence primitives. Extensions patching removed model methods (`finalize!` internals, updater hooks) should move to workflow hooks (`Spree.hooks.register('carts.complete.before_finalize') { |flow| ... }` — handlers receive the workflow instance) or event subscribers.
|
|
239
|
+
- **Store-scoped data is single-owner.** `Product`, `Promotion` and `PaymentMethod` `belongs_to :store`; the `stores: [...]` writers are gone. Multi-store sharing lives in the `spree_multi_store` extension.
|
|
240
|
+
- The Store API v3 contract is stable across the split — cart endpoints keep their shapes; `requirements` gains `code` (and renames the delivery entry's field/code to `delivery_method`, see above), and serializer `number` on carts is bridged as described above.
|
|
241
|
+
|
|
242
|
+
> **WARNING:** This guide tracks the 6.0 development line and will grow until release. If a behavioral change you hit isn't documented here, treat it as a documentation bug and report it.
|