@spree/docs 0.1.168 → 0.1.170
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.
|
@@ -279,6 +279,40 @@ is reported through `Rails.error` so it's visible rather than mysterious.
|
|
|
279
279
|
| `products.update` | `after_update` | lifecycle | After the product is saved (in transaction) |
|
|
280
280
|
| `products.destroy` | `validate` | validate | Before the product is soft-deleted — refuse a deletion your store shouldn't allow |
|
|
281
281
|
| `products.destroy` | `after_destroy` | lifecycle | After the soft-delete, for host cleanup (in transaction) |
|
|
282
|
+
| `products.activate` | `validate` | validate | Before the product goes on sale — the place to require an image, a price or a category |
|
|
283
|
+
| `products.activate` | `after_activate` | lifecycle | After the status is written (in transaction) |
|
|
284
|
+
| `products.archive` | `validate` | validate | Before the product is taken off sale |
|
|
285
|
+
| `products.archive` | `after_archive` | lifecycle | After the status is written (in transaction) |
|
|
286
|
+
| `products.draft` | `validate` | validate | Before the product returns to draft |
|
|
287
|
+
| `products.draft` | `after_draft` | lifecycle | After the status is written (in transaction) |
|
|
288
|
+
| `gift_cards.apply` | `validate` | validate | Before a card is drawn against — who may spend a card, on what, up to how much |
|
|
289
|
+
| `gift_cards.apply` | `after_apply` | lifecycle | After the store credit and payment exist (in transaction) |
|
|
290
|
+
| `gift_cards.remove` | `validate` | validate | Before a card is taken back off an order |
|
|
291
|
+
| `gift_cards.remove` | `after_remove` | lifecycle | After the balance is returned to the card (in transaction) |
|
|
292
|
+
| `gift_cards.redeem` | `validate` | validate | Before the card is recorded as spent |
|
|
293
|
+
| `gift_cards.redeem` | `after_redeem` | lifecycle | After the status is written (in transaction) |
|
|
294
|
+
| `gift_cards.cancel` | `validate` | validate | Before a card is voided |
|
|
295
|
+
| `gift_cards.cancel` | `after_cancel` | lifecycle | After the card is voided (in transaction) |
|
|
296
|
+
| `price_lists.create` | `validate` | validate | After the list is built, before it is saved |
|
|
297
|
+
| `price_lists.create` | `after_create` | lifecycle | After the list and its product membership exist (in transaction) |
|
|
298
|
+
| `price_lists.update` | `validate` | validate | With the pending attributes assigned, before anything is written |
|
|
299
|
+
| `price_lists.update` | `after_update` | lifecycle | After membership and price overrides are applied (in transaction) |
|
|
300
|
+
| `price_lists.activate` | `validate` | validate | Before a price list takes effect |
|
|
301
|
+
| `price_lists.activate` | `after_activate` | lifecycle | After the list goes live or is scheduled (in transaction) |
|
|
302
|
+
| `price_lists.deactivate` | `validate` | validate | Before a price list stops applying |
|
|
303
|
+
| `price_lists.deactivate` | `after_deactivate` | lifecycle | After the list is switched off (in transaction) |
|
|
304
|
+
| `invitations.accept` | `validate` | validate | After the expiry and invitee checks pass, before any access is granted |
|
|
305
|
+
| `invitations.accept` | `after_accept` | lifecycle | After the role is granted and the invitation marked accepted (in transaction) |
|
|
306
|
+
| `imports.start_mapping` | `validate` | validate | Before the uploaded file is read |
|
|
307
|
+
| `imports.start_mapping` | `after_start_mapping` | lifecycle | After the column mappings are built (in transaction) |
|
|
308
|
+
| `imports.complete_mapping` | `validate` | validate | Before the mapping is accepted — refuse a mapping your store considers incomplete |
|
|
309
|
+
| `imports.complete_mapping` | `after_complete_mapping` | lifecycle | After the mapping is accepted, before row creation is dispatched (in transaction) |
|
|
310
|
+
| `imports.start_processing` | `validate` | validate | Before the import starts working through its rows |
|
|
311
|
+
| `imports.start_processing` | `after_start_processing` | lifecycle | After the status is written (in transaction) |
|
|
312
|
+
| `imports.complete` | `validate` | validate | Before the import is closed out |
|
|
313
|
+
| `imports.complete` | `after_complete` | lifecycle | After the import is completed and the store touched (in transaction) |
|
|
314
|
+
| `imports.retry_failed_rows` | `validate` | validate | Before failed rows are queued again |
|
|
315
|
+
| `imports.retry_failed_rows` | `after_retry` | lifecycle | After the import returns to processing, before re-dispatch (in transaction) |
|
|
282
316
|
|
|
283
317
|
`before_cancel` and `before_resume` accept `reject!` like a `validate` hook.
|
|
284
318
|
|
|
@@ -396,11 +430,66 @@ Two rules worth internalising:
|
|
|
396
430
|
connection open across a network round trip, and a timeout leaves your database
|
|
397
431
|
and the payment processor disagreeing about what happened.
|
|
398
432
|
|
|
399
|
-
**
|
|
433
|
+
**Models get a plain `status` string, not a state machine.** Transitions are
|
|
400
434
|
workflows: `MyStore::Subscriptions::Cancel.call(...)`, not `subscription.cancel!`.
|
|
401
435
|
Transition callbacks hide side effects inside a save, cannot take arguments, and
|
|
402
436
|
have no compensation story.
|
|
403
437
|
|
|
438
|
+
As of 6.0 this is not just advice for new models — Spree has no state machines
|
|
439
|
+
left. Every status a record can hold is declared with `Spree::HasStatus`, and
|
|
440
|
+
every move between two of them is a workflow you can hook.
|
|
441
|
+
|
|
442
|
+
## Statuses
|
|
443
|
+
|
|
444
|
+
`Spree::HasStatus` declares the statuses a model can hold:
|
|
445
|
+
|
|
446
|
+
```ruby
|
|
447
|
+
class MyStore::Subscription < Spree.base_class
|
|
448
|
+
include Spree::HasStatus
|
|
449
|
+
has_status :trialing, :active, :paused, :canceled, default: :trialing
|
|
450
|
+
end
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
That gives you an inclusion validation, a predicate per value
|
|
454
|
+
(`subscription.paused?`), a scope per value (`Subscription.paused`) and
|
|
455
|
+
`with_status(:active, :trialing)` for several at once. It deliberately does
|
|
456
|
+
*not* give you a transition graph — deciding which moves are legal is the
|
|
457
|
+
workflow's job, which is what lets a transition take arguments, call out to a
|
|
458
|
+
gateway outside a transaction, and undo itself when a later step fails.
|
|
459
|
+
|
|
460
|
+
Statuses are additive, so an extension can add its own without reopening the
|
|
461
|
+
model:
|
|
462
|
+
|
|
463
|
+
```ruby
|
|
464
|
+
Spree::GiftCard.add_status(:on_hold, after: :active)
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
A custom status needs a custom workflow to move records into it. That is the
|
|
468
|
+
design, not a gap: a central place validating transitions would be a state
|
|
469
|
+
machine again.
|
|
470
|
+
|
|
471
|
+
> **NOTE:** `has_status` never overwrites something the model already defines. Where a
|
|
472
|
+
> status name means more than the column value — `Spree::GiftCard#active?` also
|
|
473
|
+
> requires the card not to have expired, and `Spree::GiftCard.active` includes
|
|
474
|
+
> partially redeemed cards — the model's own definition wins and the generated
|
|
475
|
+
> one is skipped.
|
|
476
|
+
|
|
477
|
+
### Moving a record between statuses
|
|
478
|
+
|
|
479
|
+
Call the workflow, not the model:
|
|
480
|
+
|
|
481
|
+
```ruby
|
|
482
|
+
result = Spree.product_archive_workflow.call(product: product)
|
|
483
|
+
result.success?
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
Spree ships one workflow per transition — `Spree::Products::Activate`,
|
|
487
|
+
`Spree::GiftCards::Redeem`, `Spree::Imports::Complete`, and so on — each with
|
|
488
|
+
its own `validate` and `after_*` hooks, all in
|
|
489
|
+
[Available hooks](#available-hooks) above. Because the write and the event it
|
|
490
|
+
publishes happen in the same place, registering against a hook is enough to
|
|
491
|
+
see every transition, wherever it was triggered from.
|
|
492
|
+
|
|
404
493
|
## Observability
|
|
405
494
|
|
|
406
495
|
Every step emits an `ActiveSupport::Notifications` event, so your APM sees the
|
|
@@ -384,10 +384,121 @@ The `compare_at_price` **reader** (which resolves against `cost_currency`) is un
|
|
|
384
384
|
Also note:
|
|
385
385
|
|
|
386
386
|
- **Ransack:** `default_price` is no longer a searchable association on `Variant`; query `prices` instead.
|
|
387
|
-
-
|
|
387
|
+
- **Prices in permitted params:** the dead `:price` and `:compare_at_price` entries are gone. Prices were already written as nested `prices: [{ amount:, currency: }]` under variants — the top-level keys had no writer behind them. (`Spree::PermittedAttributes` itself is removed in 6.0 — see below.)
|
|
388
388
|
- Localized number parsing still happens: `Spree::Price#amount=` runs `Spree::LocalizedNumber.parse`, so `set_price(currency, '1,599.99')` works as `price=` did.
|
|
389
389
|
- The variant validation that inferred a missing price from the product's default variant is gone. Set prices explicitly (the product and variant factories already do).
|
|
390
390
|
|
|
391
|
+
### `Spree::PermittedAttributes` is removed
|
|
392
|
+
|
|
393
|
+
The global permitted-attributes registry is gone, with no deprecation bridge. It
|
|
394
|
+
existed so the Rails admin and storefront could share one allowlist; both are
|
|
395
|
+
removed in 6.0, and API v3 declares its attributes in the controller.
|
|
396
|
+
|
|
397
|
+
Removed alongside it: `Spree::Core::ControllerHelpers::StrongParameters` (the
|
|
398
|
+
`permitted_*_attributes` helper methods it mixed into controllers) and the
|
|
399
|
+
fallback that inferred an attribute list from the model name.
|
|
400
|
+
|
|
401
|
+
Attributes you pushed from an initializer are now declared on the model, and
|
|
402
|
+
standard resource endpoints append them to their own allowlist — so one
|
|
403
|
+
declaration still covers the model's create and update endpoints.
|
|
404
|
+
|
|
405
|
+
#### How to migrate
|
|
406
|
+
|
|
407
|
+
**1. Find every call site.** The constant and the helper methods are both gone,
|
|
408
|
+
so a missed reference raises `NameError` or `NoMethodError` the first time that
|
|
409
|
+
code runs — loudly, but not necessarily at boot:
|
|
410
|
+
|
|
411
|
+
```bash
|
|
412
|
+
grep -rn "PermittedAttributes" app config lib
|
|
413
|
+
grep -rnE "permitted_[[:alnum:]_]+_attributes" app config lib
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
The second pattern is deliberately broad: the helper module generated one
|
|
417
|
+
`permitted_*_attributes` method per registry key, so there were dozens of them.
|
|
418
|
+
|
|
419
|
+
**2. Decide what each attribute actually is.** Most fall into one of three
|
|
420
|
+
buckets, and only the last needs this hook:
|
|
421
|
+
|
|
422
|
+
| What you were adding | Where it goes in 6.0 |
|
|
423
|
+
| --- | --- |
|
|
424
|
+
| A merchant-managed field (text, number, dropdown) | [Custom Fields](../core-concepts/custom-fields.md) — no code, and filterable/sortable |
|
|
425
|
+
| Config for an STI type you register (promotion rule, delivery method rule, …) | `additional_permitted_attributes` on that subclass, as before — unchanged |
|
|
426
|
+
| A real database column your extension added to a core model | `additional_permitted_attributes` on the model |
|
|
427
|
+
|
|
428
|
+
**3. Point each declaration at the model.** It stays in your initializer — only
|
|
429
|
+
the receiver changes, from the global registry to the model itself:
|
|
430
|
+
|
|
431
|
+
```ruby config/initializers/spree.rb
|
|
432
|
+
# Before
|
|
433
|
+
Spree::PermittedAttributes.product_attributes << :brand_id
|
|
434
|
+
|
|
435
|
+
# After
|
|
436
|
+
Spree::Product.additional_permitted_attributes += [:brand_id]
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
Use `+=`, not `=` — the list is per model, and assigning replaces whatever
|
|
440
|
+
another extension already added. `<<` raises a `FrozenError`: the default is a
|
|
441
|
+
frozen shared array, so mutating it in place would leak your attribute onto every
|
|
442
|
+
other model.
|
|
443
|
+
|
|
444
|
+
Declare only attributes of your own. Redeclaring a key the controller already
|
|
445
|
+
permits (`metadata`, `prices`) does not widen it — strong parameters keeps the
|
|
446
|
+
last filter for that key, so the controller's own would be replaced by yours.
|
|
447
|
+
|
|
448
|
+
Entries are `params.permit` fragments, so collections and nested structures keep
|
|
449
|
+
the shapes you already know: `[:brand_id, { region_ids: [] }]`.
|
|
450
|
+
|
|
451
|
+
**4. Fix your own controllers.** If you subclassed a Spree v3 resource
|
|
452
|
+
controller and relied on the attribute list being inferred from the model name,
|
|
453
|
+
declare it now:
|
|
454
|
+
|
|
455
|
+
```ruby
|
|
456
|
+
class BrandsController < Spree::Api::V3::Admin::ResourceController
|
|
457
|
+
protected
|
|
458
|
+
|
|
459
|
+
def model_class
|
|
460
|
+
Spree::Brand
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# Before: no such method — the base class inferred `brand_attributes`
|
|
464
|
+
# from the model name. Now you say what you accept.
|
|
465
|
+
def resource_permitted_attributes
|
|
466
|
+
[:name, :slug, :description]
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
Declaring neither `resource_permitted_attributes` nor `permitted_params` raises
|
|
472
|
+
`NotImplementedError` on the first write, so this surfaces in your test suite
|
|
473
|
+
rather than silently permitting a stale list.
|
|
474
|
+
|
|
475
|
+
**5. Verify a write actually persists.** A declaration that never reaches a
|
|
476
|
+
controller fails silently — strong parameters drop the unpermitted key, the
|
|
477
|
+
request still returns 200, and the column keeps its old value. Assert on the
|
|
478
|
+
saved record, not the response status:
|
|
479
|
+
|
|
480
|
+
```ruby
|
|
481
|
+
patch "/api/v3/admin/products/#{product.prefixed_id}",
|
|
482
|
+
params: { brand_id: brand.id }, headers: headers
|
|
483
|
+
|
|
484
|
+
expect(product.reload.brand_id).to eq(brand.id)
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
If that assertion fails, the endpoint is not consulting your declaration. Check
|
|
488
|
+
whether the controller overrides `permitted_attributes` — that method is where
|
|
489
|
+
the extension attributes are appended, so overriding it replaces them. Override
|
|
490
|
+
`resource_permitted_attributes` instead.
|
|
491
|
+
|
|
492
|
+
> **WARNING:** Two endpoints deliberately ignore the hook because their parameters are
|
|
493
|
+
> authorization data rather than resource data: API keys (`scopes`, `key_type`)
|
|
494
|
+
> and invitations (`role_id`). Adding attributes there needs a controller
|
|
495
|
+
> decorator, not a model declaration.
|
|
496
|
+
|
|
497
|
+
STI types registered through a Spree registry (promotion rules and actions,
|
|
498
|
+
delivery method rules, commission rules) already used
|
|
499
|
+
`additional_permitted_attributes` and need no changes — the hook simply moved up
|
|
500
|
+
to `Spree::Base`.
|
|
501
|
+
|
|
391
502
|
### `StateChange` and `LogEntry` are gone
|
|
392
503
|
|
|
393
504
|
`Spree::StateChange` and `Spree::LogEntry` are removed — the models, the `state_changes` associations on `Order`, `Payment` and `Fulfillment`, the `log_entries` associations on `Payment` and `Refund`, and everything that wrote to them. Both were write-only: nothing in Spree read the rows back, and the admin screens that displayed them are gone.
|