@spree/docs 0.1.248 → 0.1.249

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 (76) hide show
  1. package/dist/api-reference/admin-api/authentication.md +34 -14
  2. package/dist/api-reference/admin-api/endpoints.md +366 -14
  3. package/dist/api-reference/admin-api/errors.md +2 -2
  4. package/dist/api-reference/admin-api/introduction.md +3 -3
  5. package/dist/api-reference/admin-api/querying.md +6 -6
  6. package/dist/api-reference/store-api/monetary-amounts.md +5 -5
  7. package/dist/api-reference/webhooks-events.md +330 -335
  8. package/dist/developer/agentic/agent-skills.md +5 -2
  9. package/dist/developer/agentic/llm-docs.md +2 -1
  10. package/dist/developer/cli/admin-api.md +1 -1
  11. package/dist/developer/cli/quickstart.md +2 -2
  12. package/dist/developer/contributing/creating-an-extension.md +292 -146
  13. package/dist/developer/contributing/developing-spree.md +13 -17
  14. package/dist/developer/core-concepts/catalogs.md +2 -2
  15. package/dist/developer/core-concepts/channels.md +3 -3
  16. package/dist/developer/core-concepts/companies.md +2 -1
  17. package/dist/developer/core-concepts/delivery-setup.md +2 -2
  18. package/dist/developer/core-concepts/discounts.md +3 -3
  19. package/dist/developer/core-concepts/events.md +6 -5
  20. package/dist/developer/core-concepts/freight.md +3 -2
  21. package/dist/developer/core-concepts/fulfillments.md +10 -8
  22. package/dist/developer/core-concepts/imports-exports.md +11 -8
  23. package/dist/developer/core-concepts/inventory.md +2 -2
  24. package/dist/developer/core-concepts/media.md +14 -14
  25. package/dist/developer/core-concepts/orders.md +2 -2
  26. package/dist/developer/core-concepts/payments.md +1 -2
  27. package/dist/developer/core-concepts/products.md +6 -6
  28. package/dist/developer/core-concepts/reporting.md +4 -3
  29. package/dist/developer/core-concepts/returns-exchanges-claims.md +7 -7
  30. package/dist/developer/core-concepts/search-filtering.md +3 -3
  31. package/dist/developer/core-concepts/sellers.md +3 -3
  32. package/dist/developer/core-concepts/staff-roles.md +4 -2
  33. package/dist/developer/core-concepts/store-credits-gift-cards.md +1 -1
  34. package/dist/developer/core-concepts/stores.md +2 -2
  35. package/dist/developer/core-concepts/translations.md +12 -8
  36. package/dist/developer/core-concepts/webhooks.md +19 -18
  37. package/dist/developer/create-spree-app/quickstart.md +2 -7
  38. package/dist/developer/customization/api.md +1 -1
  39. package/dist/developer/customization/checkout.md +2 -2
  40. package/dist/developer/customization/dependencies.md +53 -37
  41. package/dist/developer/customization/permissions.md +2 -2
  42. package/dist/developer/dashboard/concepts.md +1 -1
  43. package/dist/developer/dashboard/customization/navigation.md +3 -2
  44. package/dist/developer/dashboard/customization/permissions.md +6 -6
  45. package/dist/developer/dashboard/plugins/publishing.md +4 -4
  46. package/dist/developer/dashboard/plugins/scaffolding.md +1 -1
  47. package/dist/developer/dashboard/public-api.md +1 -1
  48. package/dist/developer/dashboard/recipes/attribute-end-to-end.md +3 -18
  49. package/dist/developer/deployment/aws.md +1 -1
  50. package/dist/developer/deployment/aws_ecs.md +3 -3
  51. package/dist/developer/deployment/background_jobs.md +9 -3
  52. package/dist/developer/deployment/docker.md +1 -2
  53. package/dist/developer/deployment/emails.md +3 -1
  54. package/dist/developer/deployment/environment_variables.md +2 -2
  55. package/dist/developer/deployment/render.md +2 -2
  56. package/dist/developer/how-to/build-a-marketplace.md +2 -2
  57. package/dist/developer/how-to/custom-api-authentication.md +1 -1
  58. package/dist/developer/how-to/custom-delivery-rate-provider.md +11 -3
  59. package/dist/developer/how-to/custom-document-numbers.md +1 -1
  60. package/dist/developer/how-to/custom-order-routing.md +15 -14
  61. package/dist/developer/how-to/custom-payment-method.md +17 -19
  62. package/dist/developer/how-to/custom-promotion.md +4 -4
  63. package/dist/developer/how-to/custom-search-provider.md +12 -5
  64. package/dist/developer/how-to/custom-stock-splitter.md +25 -24
  65. package/dist/developer/how-to/sell-digital-products.md +1 -1
  66. package/dist/developer/multi-tenant/quickstart.md +2 -2
  67. package/dist/developer/providers/payouts.md +6 -2
  68. package/dist/developer/sdk/admin/querying-and-errors.md +1 -1
  69. package/dist/developer/sdk/admin/quickstart.md +4 -4
  70. package/dist/developer/sdk/authentication.md +5 -2
  71. package/dist/developer/sdk/store/cart-checkout.md +4 -4
  72. package/dist/developer/storefront/nextjs/emails.md +4 -2
  73. package/dist/developer/storefront/nextjs/testing.md +1 -1
  74. package/dist/developer/upgrades/5.6-to-6.0.md +51 -20
  75. package/dist/integrations/search/meilisearch.md +4 -4
  76. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: "Agent Skills"
3
3
  sidebarTitle: Agent Skills
4
- description: "Teach your AI coding agent Spree's conventions — 25 installable skills, a Spree expert subagent, slash commands, and safety hooks."
4
+ description: "Teach your AI coding agent Spree's conventions — 38 installable skills, a Spree expert subagent, slash commands, and safety hooks."
5
5
  ---
6
6
 
7
7
  [spree/agent-skills](https://github.com/spree/agent-skills) is a collection of agent skills for Spree Commerce. Skills are markdown instructions your coding agent loads on demand — they encode Spree's conventions, customization decision trees, API patterns, and upgrade flows, so the agent does things the Spree way instead of improvising from training data.
@@ -10,6 +10,9 @@ The skills work with Claude Code, Codex, Cursor, GitHub Copilot, Cline, Aider, Z
10
10
 
11
11
  ## Install
12
12
 
13
+ > **NOTE:** The skills target Spree 6. For a Spree 5 project, use the [`v0.3.0` release](https://github.com/spree/agent-skills/tree/v0.3.0) of the skills.
14
+
15
+
13
16
  Run from your project directory:
14
17
 
15
18
  ```bash
@@ -30,7 +33,7 @@ npx skills update # update installed skil
30
33
 
31
34
  | | |
32
35
  |---|---|
33
- | **25 skills** | Project conventions, the customization decision tree (subscriber vs decorator vs dependency injection vs generator), the v3 Store + Admin API, the `spree:model` / `spree:api_resource` generators, TypeScript SDKs, upgrades, and domain deep-dives: catalog, checkout, payments, promotions, pricing, shipping, i18n, testing, security, performance, deployment. |
36
+ | **38 skills** | Project conventions, the customization decision tree (workflow hook vs event subscriber vs provider vs dependency swap vs decorator), workflows and hooks, the v3 Store, Admin and Seller APIs, the `spree:model` / `spree:api_resource` generators, TypeScript SDKs, staff permissions, the admin dashboard and plugins, upgrades (including 5.6 → 6.0), and domain deep-dives: catalog, pricing, inventory, checkout, order totals, taxes, payments, promotions, fulfillment, returns, marketplace, B2B, multi-tenant, reporting, i18n, testing, security, performance, deployment. |
34
37
  | **`spree-expert` subagent** | A Claude Code agent for multi-step investigations (upgrade audits, checkout-flow debugging, API surface planning). It researches in its own context window and returns a focused report, keeping your main session lean. |
35
38
  | **2 slash commands** | `/spree:doctor` diagnoses the local dev stack (containers, env, migrations, job queues) and prescribes fixes. `/spree:audit-upgrade [version]` runs a read-only upgrade-readiness audit against your codebase. |
36
39
  | **2 safety hooks** | Block destructive database commands (`db:drop`, `DROP TABLE spree_*`, mass deletes, force-pushes to main) and warn when an edit introduces a hardcoded secret (Stripe live keys, AWS keys, API tokens). Disable with `SPREE_HOOKS_DISABLE=1`. |
@@ -40,7 +40,8 @@ node_modules/@spree/docs/dist/
40
40
  │ ├── customization/ # decorators, dependencies, events, …
41
41
  │ ├── storefront/ # Next.js storefront guides
42
42
  │ ├── sdk/ # TypeScript SDK docs
43
- │ ├── admin/ # admin customization, form builder, components
43
+ │ ├── dashboard/ # React admin dashboard customization and plugins
44
+ │ ├── providers/ # tax, delivery, payment, search and other providers
44
45
  │ └── how-to/ # focused guides
45
46
  └── api-reference/
46
47
  └── store.yaml # the full Store API OpenAPI spec
@@ -52,7 +52,7 @@ For remote stores, create a secret key in the admin under **Settings → API Key
52
52
 
53
53
  ```bash
54
54
  spree auth login --profile prod --base-url https://store.example.com
55
- spree api get /orders --profile prod -q "status_eq=complete"
55
+ spree api get /orders --profile prod -q "status_eq=placed"
56
56
  spree auth status
57
57
  spree auth logout --profile prod
58
58
  ```
@@ -11,7 +11,7 @@ The Spree CLI (`@spree/cli`) does two things from your terminal:
11
11
 
12
12
  ```bash
13
13
  spree dev # boot the project
14
- spree api get /orders -q status_eq=complete --limit 10 # query the Admin API
14
+ spree api get /orders -q status_eq=placed --limit 10 # query the Admin API
15
15
  spree api post /products -d '{"name":"Classic Tee","prices":[{"currency":"USD","amount":"29.99"}]}' # create a resource
16
16
  spree api endpoints --search refund # discover endpoints, offline
17
17
  ```
@@ -229,7 +229,7 @@ Run a generator inside the container. Bare generator names are auto-prefixed wit
229
229
  ```bash
230
230
  spree generate model Brand name:string slug:string:uniq # runs spree:model
231
231
  spree generate api_resource Brand name:string slug:string:uniq # runs spree:api_resource — model + v3 Store/Admin API
232
- spree generate subscriber OmsOrderSync order.completed # runs spree:subscriber — event subscriber + registration
232
+ spree generate subscriber OmsOrderSync order.placed # runs spree:subscriber — event subscriber + registration
233
233
  spree generate migration AddPositionToSpreeBrands position:integer # Rails built-in, forwarded as-is
234
234
  ```
235
235
 
@@ -1,265 +1,411 @@
1
1
  ---
2
2
  title: Creating a Spree extension
3
- description: Step-by-step guide to building a Spree extension gem — scaffolding, decorators, migrations, assets, and packaging for distribution on GitHub.
3
+ description: Build a Spree extension gem — models, API endpoints, permissions, workflow hooks and event subscribers — and pair it with a dashboard plugin for the admin UI.
4
4
  ---
5
5
 
6
6
  ## Overview
7
7
 
8
- Spree Extensions are a way to add new functionality to your Spree store. They are a great way to extend the functionality of Spree and add new features. You can share them with Spree community on Github so anyone can use them, contribute back and share your improvements. For officially supported integrations, see the [Integrations directory](/integrations).
8
+ A Spree extension is a Ruby gem containing a Rails engine. It adds features to any Spree application without forking Spree: new models, new API endpoints, new permissions, and custom logic that runs inside Spree's workflows. You can share an extension with the Spree community on GitHub so anyone can use it and contribute improvements. For officially supported integrations, see the [Integrations directory](/integrations).
9
9
 
10
- > **INFO:** This tutorial uses decorators for extending Spree models. For extensions that need to react to events (sync with external services, send notifications, etc.), consider using [Events subscribers](../core-concepts/events.md) instead - they're easier to test and maintain. See [Customization Quickstart](../customization/quickstart.md) for guidance on choosing the right approach.
10
+ An extension is backend code only. Spree 6 has no Rails storefront or Rails admin to add views to:
11
11
 
12
- ## Getting Started
12
+ | Part of the feature | Where it lives |
13
+ |---|---|
14
+ | Models, migrations, business logic | The extension gem |
15
+ | API endpoints (Store API and Admin API) | The extension gem |
16
+ | Permissions for staff roles and API keys | The extension gem |
17
+ | Admin screens | A [dashboard plugin](../dashboard/plugins/overview.md) — an npm package that talks to your Admin API endpoints |
18
+ | Storefront pages | Your storefront application, through [`@spree/sdk`](../sdk/quickstart.md) |
13
19
 
14
- Let's build a simple extension. Suppose we want the ability to mark certain products as being on sale. We'd like to be able to set a sale price on a product and show products that are on sale on a separate products page. This is a great example of how an extension can be used to build on the solid Spree foundation.
20
+ > **INFO:** Prefer Spree's extension points over [decorators](../customization/decorators.md): [workflow hooks](../customization/workflows.md) to change what happens inside a flow, [events](../core-concepts/events.md) to react after something happened, and `additional_permitted_attributes` to make your own columns writable. They keep working when Spree changes internally. See the [Customization Quickstart](../customization/quickstart.md) for guidance on choosing the right approach.
15
21
 
16
- Install the Spree Extension CLI by running:
22
+ In this guide we build `spree_reviews`: customers read product reviews through the Store API, and staff manage them through the Admin API.
17
23
 
24
+ ## Generate the extension
25
+
26
+ Install the Spree extension generator:
18
27
 
19
28
  ```bash
20
29
  gem install spree_extension
21
30
  ```
22
31
 
23
- Run the following command from a directory of your choice outside of our Spree application:
32
+ Run the following command from a directory outside your Spree application:
24
33
 
25
34
  ```bash
26
- spree-extension simple_sales
35
+ spree-extension create reviews
27
36
  ```
28
37
 
29
- This creates a `spree_simple_sales` directory with several additional files and directories. After generating the extension make sure you change to its directory:
38
+ This creates a `spree_reviews` directory containing the engine, a gemspec, a test setup and continuous integration configuration. Change to its directory:
30
39
 
31
40
  ```bash
32
- cd spree_simple_sales
41
+ cd spree_reviews
33
42
  ```
34
43
 
35
- ## Adding a Sale Price to Variants
36
-
37
- The first thing we need to do is create a migration that adds a sale_price column to [variants](../core-concepts/products.md#variants).
38
-
39
- We can do this with the following command:
44
+ > **WARNING:** The generated scaffold still declares a dependency on `spree_admin`, the Spree 5 Rails admin, which does not exist in Spree 6. Remove the `spree_admin` lines from both the `.gemspec` and the `Gemfile` before running `bundle install`. The JavaScript, importmap and asset setup in the scaffold is only used by the Spree 5 admin, so you can delete it too.
40
45
 
41
- ```bash
42
- bin/rails g migration add_sale_price_to_spree_variants sale_price:decimal
43
- ```
46
+ ## Add a model
44
47
 
45
- Because we are dealing with prices, we need to now edit the generated migration to ensure the correct precision and scale. Edit the file `db/migrate/XXXXXXXXXXX_add_sale_price_to_spree_variants.rb` so that it contains the following:
48
+ Create the migration in `db/migrate/`:
46
49
 
47
- ```ruby
48
- class AddSalePriceToSpreeVariants < ActiveRecord::Migration[7.1]
50
+ ```ruby db/migrate/20260901000000_create_spree_reviews.rb
51
+ class CreateSpreeReviews < ActiveRecord::Migration[8.1]
49
52
  def change
50
- add_column :spree_variants, :sale_price, :decimal, precision: 8, scale: 2
53
+ create_table :spree_reviews do |t|
54
+ t.references :store, null: false, index: true, foreign_key: false
55
+ t.references :product, null: false, index: true, foreign_key: false
56
+ t.integer :rating, null: false
57
+ t.text :body
58
+ t.boolean :approved, null: false, default: false
59
+
60
+ t.timestamps
61
+ end
51
62
  end
52
63
  end
53
64
  ```
54
65
 
55
- ## Adding Our Extension to the Spree Application
66
+ Then the model:
56
67
 
57
- Before we continue development of our extension, let's add it to the Spree application.
68
+ ```ruby app/models/spree/review.rb
69
+ module Spree
70
+ class Review < Spree.base_class
71
+ include Spree::SingleStoreResource
58
72
 
59
- Within the `my_store` application directory, add the following line to the bottom of our `Gemfile`:
73
+ has_prefix_id :review
74
+ publishes_lifecycle_events
60
75
 
61
- ```ruby
62
- gem 'spree_simple_sales', path: '../spree_simple_sales'
76
+ belongs_to :product, class_name: 'Spree::Product'
77
+
78
+ validates :rating, presence: true, inclusion: { in: 1..5 }
79
+
80
+ scope :approved, -> { where(approved: true) }
81
+
82
+ self.whitelisted_ransackable_attributes = %w[rating approved]
83
+ end
84
+ end
63
85
  ```
64
86
 
65
- You may have to adjust the path somewhat depending on where you created the extension. You want this to be the path relative to the location of the `my_store` application.
87
+ A few Spree conventions are at work here:
66
88
 
67
- Once you have added the gem, it's time to bundle:
89
+ - **Reviews belong to a store.** `Spree::SingleStoreResource` fills in the store for new records, and the API only returns the current store's reviews. Commerce data is always per store.
90
+ - **Prefixed IDs.** `has_prefix_id` gives every review an ID like `review_k5nR8xLq` in API responses. Integer database IDs are never exposed.
91
+ - **No foreign key constraints** on business tables, and `class_name` on every association.
92
+ - **Lifecycle events.** `publishes_lifecycle_events` makes the model emit `review.created`, `review.updated` and `review.deleted`, which subscribers and [webhooks](../core-concepts/webhooks.md) can react to.
68
93
 
69
- ```bash
70
- bundle install
94
+ ## Expose it through the API
95
+
96
+ Add serializers for both APIs. The Admin API serializer extends the Store API one with the fields only staff should see:
97
+
98
+ ```ruby app/serializers/spree/api/v3/review_serializer.rb
99
+ module Spree
100
+ module Api
101
+ module V3
102
+ class ReviewSerializer < BaseSerializer
103
+ typelize rating: :number, body: [:string, nullable: true]
104
+
105
+ attributes :rating, :body
106
+ end
107
+ end
108
+ end
109
+ end
71
110
  ```
72
111
 
73
- Finally, let's run the `spree_simple_sales` install generator to copy over the migration we just created. Answer **yes** if prompted to run migrations:
112
+ ```ruby app/serializers/spree/api/v3/admin/review_serializer.rb
113
+ module Spree
114
+ module Api
115
+ module V3
116
+ module Admin
117
+ class ReviewSerializer < V3::ReviewSerializer
118
+ typelize approved: :boolean
119
+
120
+ attributes :approved, :created_at, :updated_at
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
126
+ ```
74
127
 
75
- ```bash
76
- # context: Your Spree store's app root (i.e. Rails.root); not the extension's root path.
77
- bin/rails g spree_simple_sales:install
128
+ The Store API controller is read-only and returns approved reviews only:
129
+
130
+ ```ruby app/controllers/spree/api/v3/store/reviews_controller.rb
131
+ module Spree
132
+ module Api
133
+ module V3
134
+ module Store
135
+ class ReviewsController < ResourceController
136
+ protected
137
+
138
+ def model_class
139
+ Spree::Review
140
+ end
141
+
142
+ def serializer_class
143
+ Spree::Api::V3::ReviewSerializer
144
+ end
145
+
146
+ def scope
147
+ super.approved
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
78
154
  ```
79
155
 
80
- ## Adding a Controller Action to HomeController
156
+ The Admin API controller gives staff full create, read, update and delete access:
81
157
 
82
- Now we need to extend `Spree::HomeController` and add an action that selects "on sale" products.
158
+ ```ruby app/controllers/spree/api/v3/admin/reviews_controller.rb
159
+ module Spree
160
+ module Api
161
+ module V3
162
+ module Admin
163
+ class ReviewsController < ResourceController
164
+ scoped_resource :reviews
83
165
 
84
- Note for the sake of this example that \`Spree::HomeController\` is only included in spree_storefront so you need to make it a dependency on your extensions \*.gemspec file.
166
+ protected
85
167
 
86
- Make sure you are in the `spree_simple_sales` root directory and run the following command to create the directory structure for our controller decorator:
168
+ def model_class
169
+ Spree::Review
170
+ end
87
171
 
88
- ```bash
89
- mkdir -p app/controllers/spree_simple_sales
172
+ def serializer_class
173
+ Spree::Api::V3::Admin::ReviewSerializer
174
+ end
175
+
176
+ def resource_permitted_attributes
177
+ [:product_id, :rating, :body, :approved]
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
90
184
  ```
91
185
 
92
- Next, create a new file in the directory we just created called `home_controller_decorator.rb` and add the following content to it:
186
+ `scoped_resource :reviews` means only staff roles and secret API keys holding the `read_reviews` or `write_reviews` permission can call these endpoints. The next section registers that permission.
187
+
188
+ Finally, add the routes. The scaffold's `config/routes.rb` already contains the route hook:
93
189
 
94
- ```ruby
95
- module SpreeSimpleSales
96
- module HomeControllerDecorator
97
- def sale
98
- @products = Spree::Product.joins(:variants_including_master).where.not(sale_price: nil).distinct
190
+ ```ruby config/routes.rb
191
+ Spree::Core::Engine.add_routes do
192
+ namespace :api, defaults: { format: 'json' } do
193
+ namespace :v3 do
194
+ namespace :store do
195
+ resources :reviews, only: [:index, :show]
196
+ end
197
+
198
+ namespace :admin do
199
+ resources :reviews
200
+ end
99
201
  end
100
202
  end
101
203
  end
102
-
103
- Spree::HomeController.prepend SpreeSimpleSales::HomeControllerDecorator
104
204
  ```
105
205
 
106
- This will select just the products that have a variant with a `sale_price` set.
206
+ Pagination, [Ransack](https://github.com/activerecord-hackery/ransack) filtering (`?q[rating_gteq]=4`), sorting and prefixed ID lookup all come from the base `ResourceController`. See [Customizing the API](../customization/api.md) for everything you can override.
107
207
 
108
- We also need to add a route to this action in our `config/routes.rb` file. Let's do this now. Update the routes file to contain the following:
208
+ ## Register permissions
109
209
 
110
- ```ruby
111
- Spree::Core::Engine.routes.draw do
112
- get "/sale" => "home#sale"
113
- end
210
+ Register your model as a permission scope, so the `read_reviews` and `write_reviews` permissions appear in the staff role editor and can be granted to secret API keys:
211
+
212
+ ```ruby config/initializers/spree.rb
213
+ Spree.permissions.register_scope(:reviews, group: :catalog, resources: -> {
214
+ [Spree::Review]
215
+ })
216
+ ```
217
+
218
+ Label the permission for the dashboard in your locale file:
219
+
220
+ ```yaml config/locales/en.yml
221
+ en:
222
+ spree:
223
+ permissions_catalog:
224
+ resources:
225
+ reviews:
226
+ label: Reviews
227
+ description: Product reviews left by customers
114
228
  ```
115
229
 
116
- ## Viewing On Sale Products
230
+ See [Permissions](../customization/permissions.md) for audiences and read-only scopes.
117
231
 
118
- ### Setting the Sale Price for a Variant
232
+ ## Extend core models and behavior
119
233
 
120
- Now that our variants have the attribute `sale_price` available to them, let's update the sample data so we have at least one product that is on sale in our application. We will need to do this in the rails console for the time being, as we have no admin interface to set sale prices for variants. So, in order to do this, first open up the rails console:
234
+ Extensions often need to change Spree's own resources as well as add new ones. Reach for these options in order.
121
235
 
122
- ```bash
123
- bin/rails c
236
+ ### Make a new column writable
237
+
238
+ Suppose each product gets a `reviews_enabled` switch. Add the column with a migration, then let the existing product endpoints accept it — no controller changes needed:
239
+
240
+ ```ruby lib/spree_reviews/engine.rb
241
+ config.to_prepare do
242
+ Spree::Product.additional_permitted_attributes += [:reviews_enabled]
243
+ end
124
244
  ```
125
245
 
126
- Now, follow the steps I take in selecting a product and updating its master variant to have a sale price. Note, you may not be editing the exact same product as I am, but this is not important. We just need one "on sale" product to display on the sales page.
246
+ Always append with `+=`, so you don't remove attributes that other extensions added.
247
+
248
+ ### Run code inside a Spree workflow
127
249
 
128
- ```ruby
129
- > product = Spree::Product.first
130
- => #<Spree::Product id: 107377505, name: "Spree Bag", description: "Lorem ipsum dolor sit amet, consectetuer adipiscing...", available_on: "2013-02-13 18:30:16", deleted_at: nil, permalink: "spree-bag", meta_description: nil, meta_keywords: nil, tax_category_id: 25484906, shipping_category_id: nil, count_on_hand: 10, created_at: "2013-02-13 18:30:16", updated_at: "2013-02-13 18:30:16", on_demand: false>
250
+ [Workflow hooks](../customization/workflows.md) run your code at a named point inside a core flow — checkout completion, cancellations, refunds, product changes — and can stop the operation. For example, to require a description before a product with reviews enabled goes on sale:
131
251
 
132
- > variant = product.master
133
- => #<Spree::Variant id: 833839126, sku: "SPR-00012", weight: nil, height: nil, width: nil, depth: nil, deleted_at: nil, is_master: true, product_id: 107377505, count_on_hand: 10, cost_price: #<BigDecimal:7f8dda5eebf0,'0.21E2',9(36)>, position: nil, lock_version: 0, on_demand: false, cost_currency: nil, sale_price: nil>
252
+ ```ruby config/initializers/spree.rb
253
+ Spree.hooks.register('products.activate.validate', 'SpreeReviews::RequireDescription')
254
+ ```
134
255
 
135
- > variant.sale_price = 8.00
136
- => 8.0
256
+ ```ruby app/services/spree_reviews/require_description.rb
257
+ module SpreeReviews
258
+ class RequireDescription
259
+ def call(workflow)
260
+ product = workflow.product
261
+ return unless product.reviews_enabled
262
+ return if product.description.present?
137
263
 
138
- > variant.save
139
- => true
264
+ workflow.reject!('Add a description before activating a product with reviews.')
265
+ end
266
+ end
267
+ end
140
268
  ```
141
269
 
142
- ## Decorating Variants
270
+ Hook names are checked when the application boots, so a typo fails at startup rather than silently never running.
143
271
 
144
- Let's fix our extension so that it uses the `sale_price` when it is present.
272
+ ### React to events
145
273
 
146
- Next, create the file `app/models/spree_simple_sales/variant_decorator.rb` and add the following content to it:
274
+ To do something *after* an event, without influencing it, write an [event subscriber](../core-concepts/events.md). For example, to ask customers for a review once their order arrives:
147
275
 
148
- ```ruby
149
- module SpreeSimpleSales
150
- module VariantDecorator
151
- def price_in(currency)
152
- return super unless sale_price.present?
153
- Spree::Price.new(variant_id: self.id, amount: self.sale_price, currency: currency)
276
+ ```ruby app/subscribers/spree_reviews/review_request_subscriber.rb
277
+ module SpreeReviews
278
+ class ReviewRequestSubscriber < Spree::Subscriber
279
+ subscribes_to 'order.delivered'
280
+
281
+ def handle(event)
282
+ order = Spree::Order.find_by_prefix_id(event.payload['id'])
283
+ return unless order
284
+
285
+ SpreeReviews::ReviewRequestMailer.request_review(order).deliver_later
154
286
  end
155
287
  end
156
288
  end
289
+ ```
290
+
291
+ Subscribers are not discovered automatically. Register them from the engine:
157
292
 
158
- Spree::Variant.prepend SpreeSimpleSales::VariantDecorator
293
+ ```ruby lib/spree_reviews/engine.rb
294
+ config.after_initialize do
295
+ Spree.subscribers << SpreeReviews::ReviewRequestSubscriber
296
+ end
159
297
  ```
160
298
 
161
- If there is a `sale_price` present on the product's master variant, we return that price. Otherwise, we call the original implementation of `price_in` using `return super`.
299
+ ### Decorators, as a last resort
162
300
 
163
- ## Testing Our Decorator
301
+ When none of the above fits — adding an association to a core model, for example — use a [decorator](../customization/decorators.md):
164
302
 
165
- It's always a good idea to test your code. We should be extra careful to write tests for our Variant decorator since we are modifying core Spree functionality. Let's write a couple of simple unit tests for `variant_decorator.rb`
303
+ ```ruby app/models/spree_reviews/product_decorator.rb
304
+ module SpreeReviews
305
+ module ProductDecorator
306
+ def self.prepended(base)
307
+ base.has_many :reviews, class_name: 'Spree::Review', dependent: :destroy
308
+ end
309
+ end
310
+ end
166
311
 
167
- ### Generating the Test App
312
+ Spree::Product.prepend SpreeReviews::ProductDecorator
313
+ ```
168
314
 
169
- An extension is not a full Rails application, so we need something to test our extension against. By running the Spree `test_app` rake task, we can generate a barebones Spree application within our `spec` directory to run our tests against.
315
+ Decorators are not loaded automatically. Load them from your engine:
170
316
 
171
- We can do this with the following command from the root directory of our extension:
317
+ ```ruby lib/spree_reviews/engine.rb
318
+ config.to_prepare do
319
+ Dir.glob(root.join('app/**/*_decorator*.rb')) do |decorator|
320
+ Rails.configuration.cache_classes ? require(decorator) : load(decorator)
321
+ end
322
+ end
323
+ ```
324
+
325
+ ## Add admin screens
326
+
327
+ Admin screens are built as a [dashboard plugin](../dashboard/plugins/overview.md): a React package that registers navigation entries, pages and product page widgets, and reads and writes data through your Admin API endpoints. Scaffold one with the Spree CLI:
172
328
 
173
329
  ```bash
174
- bundle exec rake test_app
330
+ npx @spree/cli plugin new reviews
175
331
  ```
176
332
 
177
- After this command completes, you should be able to run `rspec` and see the following output:
333
+ See [Scaffolding](../dashboard/plugins/scaffolding.md) and [Backend integration](../dashboard/customization/backend.md) to connect it to the endpoints above, and [Distributing](../dashboard/plugins/distributing.md) to ship the gem and the npm package together.
178
334
 
179
- ```bash
180
- No examples found.
335
+ ## Add the extension to your application
336
+
337
+ From your Spree application's backend directory, add the gem to the `Gemfile`. Adjust the path to point at your extension:
181
338
 
182
- Finished in 0.00005 seconds
183
- 0 examples, 0 failures
339
+ ```ruby Gemfile
340
+ gem 'spree_reviews', path: '../spree_reviews'
184
341
  ```
185
342
 
186
- Great! We're ready to start adding some tests. Let's replicate the extension's directory structure in our spec directory by running the following command
343
+ Install it, then copy the extension's migrations into the application and run them:
187
344
 
188
345
  ```bash
189
- mkdir -p spec/models/spree
346
+ bundle install
347
+ bin/rails g spree_reviews:install
190
348
  ```
191
349
 
192
- Now, let's create a new file in this directory called `variant_decorator_spec.rb` and add the following tests to it:
350
+ The Store API now serves reviews at `/api/v3/store/reviews`, and the Admin API manages them at `/api/v3/admin/reviews`.
193
351
 
194
- ```ruby
195
- require 'spec_helper'
196
-
197
- describe Spree::Variant do
198
- describe "#price_in" do
199
- it "returns the sale price if it is present" do
200
- variant = create(:variant, sale_price: 8.00)
201
- expected = Spree::Price.new(variant_id: variant.id, currency: "USD", amount: variant.sale_price)
352
+ ## Test the extension
202
353
 
203
- result = variant.price_in("USD")
354
+ An extension is not a full Rails application, so its tests run against a small generated Spree application. Create it from the extension's root directory:
204
355
 
205
- expect(result.variant_id).to eq(expected.variant_id)
206
- expect(result.amount.to_f).to eq(expected.amount.to_f)
207
- expect(result.currency).to eq(expected.currency)
208
- end
209
-
210
- it "returns the normal price if it is not on sale" do
211
- variant = create(:variant, price: 15.00)
212
- expected = Spree::Price.new(variant_id: variant.id, currency: "USD", amount: variant.price)
356
+ ```bash
357
+ bundle exec rake test_app
358
+ ```
213
359
 
214
- result = variant.price_in("USD")
360
+ Run this again whenever you add a migration. Tests use RSpec and Factory Bot, with Spree's factories and test helpers coming from [`spree_dev_tools`](https://github.com/spree/spree_dev_tools). Add a factory for your model:
215
361
 
216
- expect(result.variant_id).to eq(expected.variant_id)
217
- expect(result.amount.to_f).to eq(expected.amount.to_f)
218
- expect(result.currency).to eq(expected.currency)
219
- end
362
+ ```ruby lib/spree_reviews/factories.rb
363
+ FactoryBot.define do
364
+ factory :review, class: Spree::Review do
365
+ product
366
+ rating { 5 }
367
+ approved { true }
220
368
  end
221
369
  end
222
370
  ```
223
371
 
224
- These specs test that the `price_in` method we overrode in our `VariantDecorator` returns the correct price both when the sale price is present and when it is not.
372
+ Then test your endpoints:
225
373
 
226
- ## Summary
227
-
228
- In this tutorial, you learned how to both install extensions and create your own. A lot of core Spree development concepts were covered and you gained exposure to some of the Spree internals.
374
+ ```ruby spec/controllers/spree/api/v3/admin/reviews_controller_spec.rb
375
+ require 'spec_helper'
229
376
 
230
- ## Alternative Approaches
377
+ RSpec.describe Spree::Api::V3::Admin::ReviewsController, type: :controller do
378
+ render_views
379
+ routes { Spree::Core::Engine.routes }
231
380
 
232
- While this tutorial uses decorators to extend Spree's core behavior, modern Spree provides additional patterns that may be more appropriate depending on your use case:
381
+ include_context 'API v3 Admin authenticated'
233
382
 
234
- | Use Case | Recommended Approach |
235
- |----------|---------------------|
236
- | Structural changes (associations, validations) | Decorators (as shown in this tutorial) |
237
- | React to model changes | [Events subscribers](../core-concepts/events.md) |
238
- | External service integration | [Webhooks](../core-concepts/webhooks.md) |
239
- | Replace core services | [Dependencies injection](../customization/dependencies.md) |
240
- | Add admin UI elements | [Admin Partials](../dashboard/customization/quickstart.md) |
241
- | Add admin menu items | [Admin Navigation](../dashboard/customization/navigation.md) |
383
+ let!(:review) { create(:review, store: store) }
242
384
 
243
- For example, if your extension needs to sync data with an external service when products are updated, use an Events subscriber instead of a decorator callback:
385
+ before { request.headers.merge!(headers) }
244
386
 
245
- ```ruby app/subscribers/my_extension/product_sync_subscriber.rb
246
- module MyExtension
247
- class ProductSyncSubscriber < Spree::Subscriber
248
- subscribes_to 'product.updated'
387
+ it 'lists reviews' do
388
+ get :index, as: :json
249
389
 
250
- def handle(event)
251
- product = Spree::Product.find_by_prefix_id(event.payload['id'])
252
- return unless product
253
-
254
- ExternalService.sync(product)
255
- end
390
+ expect(response).to have_http_status(:ok)
391
+ expect(json_response['data'].map { |r| r['id'] }).to include(review.prefixed_id)
256
392
  end
257
393
  end
258
394
  ```
259
395
 
260
- ## Related Documentation
396
+ Run the suite:
397
+
398
+ ```bash
399
+ bundle exec rspec
400
+ ```
401
+
402
+ Test the logic you wrote — the approved-only scope, your hook handler, your subscriber — rather than behavior that Rails and Spree already guarantee.
403
+
404
+ ## Related documentation
261
405
 
406
+ - [Customization Quickstart](../customization/quickstart.md) - Choose the right extension point
407
+ - [Customizing the API](../customization/api.md) - Controllers, serializers and permitted attributes
408
+ - [Services & Workflows](../customization/workflows.md) - The full list of workflow hooks
262
409
  - [Events](../core-concepts/events.md) - Subscribe to Spree events
263
- - [Webhooks](../core-concepts/webhooks.md) - HTTP callbacks for external integrations
264
- - [Dependencies](../customization/dependencies.md) - Replace core services
265
- - [Customization Quickstart](../customization/quickstart.md) - Choose the right approach
410
+ - [Permissions](../customization/permissions.md) - Register permission scopes
411
+ - [Dashboard plugins](../dashboard/plugins/overview.md) - Admin UI for your extension