@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
- **New models get a plain `status` string, not a state machine.** Transitions are
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
- - **`Spree::PermittedAttributes`:** the dead `:price` and `:compare_at_price` entries are dropped from `product_attributes` and `variant_attributes`. Prices were already written as nested `prices: [{ amount:, currency: }]` under variants — the top-level keys had no writer behind them.
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spree/docs",
3
- "version": "0.1.168",
3
+ "version": "0.1.170",
4
4
  "description": "Spree Commerce developer documentation for AI agents and local reference",
5
5
  "type": "module",
6
6
  "license": "CC-BY-4.0",