@spree/docs 0.1.236 → 0.1.238

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 (48) hide show
  1. package/dist/api-reference/admin-api/authentication.md +45 -0
  2. package/dist/api-reference/store.yaml +130 -130
  3. package/dist/api-reference/webhooks-events.md +1 -1
  4. package/dist/developer/contributing/developing-spree.md +2 -2
  5. package/dist/developer/core-concepts/metafields.md +1 -1
  6. package/dist/developer/core-concepts/store-credits-gift-cards.md +3 -5
  7. package/dist/developer/core-concepts/stores.md +1 -1
  8. package/dist/developer/customization/configuration.md +8 -8
  9. package/dist/developer/customization/dependencies.md +9 -9
  10. package/dist/developer/customization/metadata.md +3 -3
  11. package/dist/developer/customization/quickstart.md +7 -7
  12. package/dist/developer/customization/validations.md +1 -1
  13. package/dist/developer/dashboard/customization/backend.md +7 -4
  14. package/dist/developer/dashboard/customization/navigation.md +5 -3
  15. package/dist/developer/dashboard/customization/permissions.md +3 -1
  16. package/dist/developer/dashboard/customization/quickstart.md +3 -4
  17. package/dist/developer/dashboard/customization/routes.md +7 -6
  18. package/dist/developer/dashboard/customization/slots.md +7 -6
  19. package/dist/developer/dashboard/customization/tables.md +4 -2
  20. package/dist/developer/dashboard/customization/translations.md +4 -2
  21. package/dist/developer/dashboard/overview.md +5 -4
  22. package/dist/developer/dashboard/public-api.md +16 -2
  23. package/dist/developer/dashboard/recipes/attribute-end-to-end.md +2 -3
  24. package/dist/developer/dashboard/recipes/custom-form-field.md +12 -6
  25. package/dist/developer/dashboard/recipes/page-action-button.md +9 -8
  26. package/dist/developer/dashboard/recipes/sidebar-widget.md +11 -5
  27. package/dist/developer/dashboard/slots-catalog.md +1 -1
  28. package/dist/developer/how-to/build-a-marketplace.md +3 -3
  29. package/dist/developer/how-to/custom-digital-asset-provider.md +1 -1
  30. package/dist/developer/how-to/custom-document-numbers.md +3 -3
  31. package/dist/developer/how-to/custom-promotion.md +1 -1
  32. package/dist/developer/how-to/sell-digital-products.md +1 -1
  33. package/dist/developer/providers/dam.md +3 -1
  34. package/dist/developer/providers/erp.md +3 -1
  35. package/dist/developer/providers/pim.md +3 -1
  36. package/dist/developer/providers/sso.md +1 -1
  37. package/dist/developer/tutorial/dashboard.md +307 -0
  38. package/dist/developer/tutorial/events.md +66 -6
  39. package/dist/developer/tutorial/introduction.md +42 -13
  40. package/dist/developer/tutorial/model-and-api.md +187 -0
  41. package/dist/developer/tutorial/storefront.md +123 -6
  42. package/dist/developer/tutorial/testing.md +163 -7
  43. package/dist/developer/upgrades/5.6-to-6.0.md +16 -0
  44. package/package.json +1 -1
  45. package/dist/developer/tutorial/admin-api.md +0 -13
  46. package/dist/developer/tutorial/dashboard-plugin.md +0 -14
  47. package/dist/developer/tutorial/model.md +0 -13
  48. package/dist/developer/tutorial/store-api.md +0 -13
@@ -1,24 +1,53 @@
1
1
  ---
2
2
  title: "Tutorial: Build a Custom Feature"
3
- description: Build Product FAQs end to end — a custom model, an Admin API endpoint, a dashboard plugin, a Store API endpoint, and storefront rendering through the TypeScript SDK.
3
+ description: Build a complete Brands feature in Spree — model and API from one generator, a dashboard plugin, storefront rendering, events, and tests.
4
4
  ---
5
5
 
6
- This tutorial builds one complete feature — **Product FAQs**, questions and answers attached to products — through every layer of a Spree 6 application. By the end you will have a custom model, back-office management in the dashboard, and customer-facing display on the storefront.
6
+ This tutorial builds one complete feature — **Brands**, a manufacturer or label attached to products — through every layer of a Spree application. By the end you will have a custom model with its own Store and Admin API, brand management in the dashboard, brands rendered on the storefront, other systems reacting to brand changes, and tests covering all of it.
7
7
 
8
- ## The two tracks
8
+ It is written for **both backend and frontend developers**. The backend code you write is minimal and mostly generated — Spree's generators produce convention-correct models, controllers and serializers, and the TypeScript SDK gives you typed access from your storefront. Most of what you write by hand is TypeScript.
9
9
 
10
- The tutorial is organized so you only build what your feature needs:
10
+ ## What you'll build
11
11
 
12
- | Track | Chapters | Build this when |
13
- |---|---|---|
14
- | Foundation | [Model](model.md) | Always |
15
- | **A — manage it in the dashboard** | [Admin API](admin-api.md) → [Dashboard plugin](dashboard-plugin.md) | The feature is operated by staff — internal tooling, back-office data |
16
- | **B — expose it on the storefront** | [Store API](store-api.md) → [Storefront](storefront.md) | Customers see the feature |
17
- | Finishing | [Events](events.md) → [Testing](testing.md) | Always |
12
+ - A `Spree::Brand` model with a Store API and an Admin API — one generator command
13
+ - A brands screen in the dashboard: a list page, navigation and a card on the product page
14
+ - Brands on the storefront, fetched through `@spree/sdk` with generated types
15
+ - Lifecycle events other systems subscribe to, in the app or over webhooks
16
+ - Specs for the model and both APIs, plus an end-to-end pass
18
17
 
19
- An internal tool is Track A alone. A storefront feature over existing data is Track B alone. Product FAQs need both: staff write the answers, customers read them.
18
+ ## The five steps
19
+
20
+
21
+ - [1. Models & API](model-and-api.md) — Generate the model, migration, Store and Admin controllers, serializers and routes in one command
22
+ - [2. Dashboard](dashboard.md) — Scaffold the back-office UI with the CLI — a table, a form, navigation and a product slot
23
+ - [3. Storefront](storefront.md) — Regenerate SDK types and render brands on the product page in the Next.js storefront
24
+ - [4. Events](events.md) — Publish lifecycle events and consume them with subscribers and outbound webhooks
25
+ - [5. Testing](testing.md) — Model and request specs, dashboard plugin tests, and one end-to-end check
26
+
27
+
28
+ The order is deliberate. Step 1 gives every later step something real to talk to: the dashboard plugin calls the Admin API, the storefront calls the Store API, and the events in step 4 reuse the serializer the same generator produced.
20
29
 
21
30
  ## Before you start
22
31
 
23
- - A running Spree application — see the [quickstart](../getting-started/quickstart.md)
24
- - The AI tooling set up — the [agent skills and MCP server](../agentic/overview.md) make every step of this tutorial faster
32
+ You need a running Spree application. The fastest path is [create-spree-app](../create-spree-app/quickstart.md), a Docker-based project with the [Spree CLI](../cli/quickstart.md) and nothing to install locally:
33
+
34
+ ```bash
35
+ npx create-spree-app@latest my-store
36
+ ```
37
+
38
+ **Every command in this tutorial runs from the root of that project**, through the `spree` CLI. Nothing needs installing on your machine — the CLI routes each command into the right container.
39
+
40
+ The project it creates has this shape, and the paths in every chapter are relative to its root:
41
+
42
+ ```
43
+ my-store/
44
+ ├── server/ # the Server app — models, API, specs
45
+ ├── apps/
46
+ │ ├── dashboard/ # the admin SPA (React)
47
+ │ └── storefront/ # the Next.js storefront
48
+ └── docker-compose.yml
49
+ ```
50
+
51
+ > **NOTE:** **create-spree-app projects:** run `spree eject` once before starting. Fresh projects use a prebuilt Docker image; ejecting switches to the development setup that mounts your app into the container, so the files the generators create land in your project and reload as you edit them.
52
+
53
+ > **TIP:** Using an AI coding agent? Install the [Spree agent skills](../agentic/agent-skills.md) (`npx skills add spree/agent-skills`) and connect the [docs MCP server](../agentic/mcp.md) before starting. Your agent will know the conventions this tutorial teaches and can follow along with you. See [AI-assisted development](../agentic/overview.md).
@@ -0,0 +1,187 @@
1
+ ---
2
+ title: Models & API
3
+ sidebarTitle: Models & API
4
+ description: Generate the Brand model, migration, Store and Admin API controllers, serializers and routes with a single spree:api_resource command.
5
+ ---
6
+
7
+ Every feature starts with a model and the API that exposes it. They arrive together: `spree:api_resource` generates the model, its migration, a customer-facing Store API, a back-office Admin API, serializers, routes, a factory and controller specs in one command.
8
+
9
+ This matters for the rest of the tutorial. The dashboard plugin in step 2 talks to the Admin API, the storefront in step 3 talks to the Store API, and the events in step 4 reuse the serializer generated here. Getting this step right makes the other four mostly configuration.
10
+
11
+ ## Step 1: Generate the resource
12
+
13
+ A brand has a name, a slug for friendly URLs, and an active flag:
14
+
15
+ ```bash
16
+ spree generate api_resource Brand name:string:index slug:string:uniq active:boolean
17
+ ```
18
+
19
+ The generator reports what it created:
20
+
21
+ ```text
22
+ create app/models/spree/brand.rb
23
+ create db/migrate/XXXXXXXXXXXXXX_create_spree_brands.rb
24
+ create app/controllers/spree/api/v3/store/brands_controller.rb
25
+ create app/controllers/spree/api/v3/admin/brands_controller.rb
26
+ create app/serializers/spree/api/v3/brand_serializer.rb
27
+ create app/serializers/spree/api/v3/admin/brand_serializer.rb
28
+ create spec/factories/spree/brand_factory.rb
29
+ create spec/controllers/spree/api/v3/store/brands_controller_spec.rb
30
+ create spec/controllers/spree/api/v3/admin/brands_controller_spec.rb
31
+
32
+ ✓ Generated Spree::Brand API resource
33
+
34
+ Prefixed ID: brand_xxxxxxxxxx
35
+ Store API: /api/v3/store/brands (read-only)
36
+ Admin API: /api/v3/admin/brands (full CRUD)
37
+ ```
38
+
39
+ Apply the migration:
40
+
41
+ ```bash
42
+ spree migrate
43
+ ```
44
+
45
+ > **INFO:** Every command in this tutorial runs from the root of your [create-spree-app](../create-spree-app/quickstart.md) project. `spree` routes each one into the right container, so generators write into `server/` and nothing needs installing on your machine.
46
+
47
+ ### The field syntax
48
+
49
+ Attributes follow the familiar `name:type:index` form, with the Spree conventions applied for you:
50
+
51
+ | You write | You get |
52
+ |---|---|
53
+ | `name:string:index` | A non-null string column with an index |
54
+ | `slug:string:uniq` | A string column with a unique index |
55
+ | `active:boolean` | A boolean column with a default |
56
+ | `user:belongs_to` | A reference with the class name resolved |
57
+
58
+ Useful flags: `--paranoid` for soft delete, `--custom-fields` for [custom fields](../core-concepts/metafields.md) support, `--writable` to give the Store API create, update and destroy as well, and `--no-store` or `--no-admin` to generate only one side. Run the generator with `--help` for the full list.
59
+
60
+ ## Step 2: What you got
61
+
62
+ You do not need to read the generated code to use it. The command produced a complete, convention-correct API, and the contract it gives you is this:
63
+
64
+ | Surface | Endpoint | Who calls it | Default access |
65
+ |---|---|---|---|
66
+ | Store API | `/api/v3/store/brands` | Storefronts, mobile apps | Read-only |
67
+ | Admin API | `/api/v3/admin/brands` | Dashboard, integrations, scripts | Full CRUD |
68
+
69
+ Three conventions are worth knowing, because every later step depends on them:
70
+
71
+ - **Public IDs, never database IDs.** Records are addressed as `brand_k5nR8xLq`. The API accepts and returns that form everywhere.
72
+ - **Two serializers, one inheriting the other.** The Store serializer carries public fields only; the Admin serializer extends it and adds `created_at` and `updated_at`. Add a public field once and both surfaces get it. This is why customers never see back-office data by accident.
73
+ - **Filtering is allowlisted.** `name`, `slug` and `active` are queryable because the generator listed them. Anything not on that list is rejected, so a caller cannot filter by a column you did not intend to expose.
74
+
75
+ The Store API is read-only by default — a shopper cannot create a brand. Pass `--writable` if a resource genuinely needs customer writes, as carts and addresses do.
76
+
77
+ **Brands belong to a store.** Every resource you generate is scoped to one, because commerce data — catalog, configuration, orders — is always per-store. A new record picks up its store automatically, and a field marked unique is unique *within* a store, so two stores can each have a brand called `wilson`. Only genuinely global reference data, like countries, opts out with `--no-store-scoped`.
78
+
79
+ > **WARNING:** One decision is still yours: **authorization**. Give the resource a permission rule before exposing it, and read lookups through the store (`current_store.brands`) rather than the whole table. Scoping by association is the cheapest defence against one store reading another's records — filtering by role is not a substitute.
80
+
81
+ ## Step 3: Try it
82
+
83
+ Create a brand through the Admin API. You need a secret key from **Settings → API keys** in the dashboard:
84
+
85
+ ```bash
86
+ curl -X POST "http://localhost:3000/api/v3/admin/brands" \
87
+ -H "X-Spree-API-Key: sk_YOUR_KEY" \
88
+ -H "Content-Type: application/json" \
89
+ -d '{ "name": "Wilson", "slug": "wilson", "active": true }'
90
+ ```
91
+
92
+ ```json
93
+ {
94
+ "id": "brand_k5nR8xLq",
95
+ "name": "Wilson",
96
+ "slug": "wilson",
97
+ "active": true,
98
+ "created_at": "2026-01-15T10:30:00Z",
99
+ "updated_at": "2026-01-15T10:30:00Z"
100
+ }
101
+ ```
102
+
103
+ Note the flat request body. API v3 takes no `{ "brand": { ... } }` wrapper around the attributes, and the response identifies the record as `brand_k5nR8xLq` rather than by a database ID.
104
+
105
+ Now read it back from the Store API, using a publishable key from the same settings page:
106
+
107
+ ```bash
108
+ curl -H "X-Spree-API-Key: pk_YOUR_KEY" \
109
+ "http://localhost:3000/api/v3/store/brands"
110
+ ```
111
+
112
+ ```json
113
+ {
114
+ "data": [
115
+ { "id": "brand_k5nR8xLq", "name": "Wilson", "slug": "wilson", "active": true }
116
+ ],
117
+ "meta": { "count": 1, "page": 1, "pages": 1 }
118
+ }
119
+ ```
120
+
121
+ The same record, minus the timestamps. That difference is the serializer split doing its job: customers get public fields, staff get the operational ones.
122
+
123
+ Filtering works through the allowlist:
124
+
125
+ ```bash
126
+ curl -H "X-Spree-API-Key: pk_YOUR_KEY" \
127
+ "http://localhost:3000/api/v3/store/brands?q[name_cont]=wil"
128
+ ```
129
+
130
+ > **TIP:** Prefer TypeScript? The same call through the SDK is `client.request('GET', '/brands')` — step 3 covers the typed client in full.
131
+
132
+ ## Connect brands to products
133
+
134
+ A brand on its own is a list. Adding a `brand_id` to products is what makes it useful in steps 2 and 3:
135
+
136
+ ```bash
137
+ spree generate migration AddBrandToSpreeProducts brand:belongs_to
138
+ spree migrate
139
+ ```
140
+
141
+ Then connect the two models. Extending a core model like `Spree::Product` is the one place this step needs backend code — a [decorator](../customization/decorators.md) adds behavior to a core class without forking it. The generator writes the file:
142
+
143
+ ```bash
144
+ spree generate model_decorator Product
145
+ ```
146
+
147
+ Add one line inside the generated `prepended` block:
148
+
149
+ ```ruby server/app/models/spree/product_decorator.rb
150
+ base.belongs_to :brand, class_name: 'Spree::Brand', optional: true
151
+ ```
152
+
153
+ And the matching line in `app/models/spree/brand.rb`, which is your own file:
154
+
155
+ ```ruby
156
+ has_many :products, class_name: 'Spree::Product', dependent: :nullify
157
+ ```
158
+
159
+ `optional: true` lets a product exist without a brand. `dependent: :nullify` means deleting a brand clears the reference rather than deleting the products that carried it — almost always what you want for a label attached to a catalog.
160
+
161
+ Two more lines make the association usable from the API. The product serializer has to expose it, or the storefront cannot read a product's brand:
162
+
163
+ ```ruby server/app/serializers/spree/api/v3/product_serializer_decorator.rb
164
+ attribute(:brand_id) { |product| product.brand&.prefixed_id }
165
+ has_one :brand, serializer: Spree::Api::V3::BrandSerializer
166
+ ```
167
+
168
+ The model has to allow filtering by it, or `?q[brand_id_eq]=…` is rejected:
169
+
170
+ ```ruby server/app/models/spree/product_decorator.rb
171
+ base.whitelisted_ransackable_attributes |= %w[brand_id]
172
+ base.whitelisted_ransackable_associations |= %w[brand]
173
+ ```
174
+
175
+ The allowlist is deliberate: every entry is a query any caller can run, so add only what the storefront needs.
176
+
177
+ Finally, the Admin API has to accept `brand_id` on write, or the picker you build in step 2 will appear to work and save nothing. Extensions append to the model's list rather than replacing it:
178
+
179
+ ```ruby server/config/initializers/spree.rb
180
+ Spree::Product.additional_permitted_attributes += [:brand_id]
181
+ ```
182
+
183
+ Always `+=`, never `=` — assigning would drop whatever another extension added.
184
+
185
+ ## Next step
186
+
187
+ The data and both APIs are live. Now give staff a way to manage brands: [Dashboard](dashboard.md).
@@ -1,12 +1,129 @@
1
1
  ---
2
2
  title: Storefront
3
- description: Render FAQs on the product page — regenerate the SDK types, extend the TypeScript client, and build the component in the Next.js storefront.
3
+ description: Render brands on the Next.js storefront in apps/storefront — call the Store API through @spree/sdk and build the product and brand pages.
4
4
  ---
5
5
 
6
- The Store API endpoint becomes typed TypeScript and a component on the product page. This chapter closes the loop from Rails model to customer screen.
6
+ The Store API endpoint from step 1 becomes typed TypeScript and a component on the product page. This step closes the loop from the Server app to the customer’s screen.
7
7
 
8
- ## What this chapter covers
8
+ ## Step 1: Describe the resource to TypeScript
9
9
 
10
- - The type generation pipeline: serializer to TypeScript types to runtime schemas
11
- - Extending the SDK client with the new resource
12
- - Fetching and rendering FAQs on the product detail page in the Next.js storefront
10
+ The storefront lives at `apps/storefront/` and talks to the Store API through `@spree/sdk`. Built-in resources like products and carts already ship typed. A resource you added is new to the SDK, so declare its shape once and reuse it:
11
+
12
+ ```typescript apps/storefront/src/types/brand.ts
13
+ export interface Brand {
14
+ id: string
15
+ name: string
16
+ slug: string | null
17
+ active: boolean
18
+ }
19
+ ```
20
+
21
+ These are exactly the fields your Store serializer returns — the public ones, without the timestamps the Admin API adds. Keep this file in step with the serializer: it is the contract between the two halves of your project.
22
+
23
+ > **NOTE:** Spree generates the built-in SDK types from its own serializers, which is why `client.products` is typed without you doing anything. That pipeline runs inside the Spree repository, not your app, so a resource you add gets a hand-written interface like the one above.
24
+
25
+ ## Step 2: Call the endpoint
26
+
27
+ `createClient` gives you a typed Store API client. Built-in resources have methods; custom ones go through `request`, which carries the same authentication, retries and locale defaults:
28
+
29
+ ```typescript apps/storefront/src/lib/spree.ts
30
+ import { createClient } from '@spree/sdk'
31
+
32
+ export const client = createClient({
33
+ baseUrl: process.env.NEXT_PUBLIC_SPREE_API_URL!,
34
+ publishableKey: process.env.NEXT_PUBLIC_SPREE_PUBLISHABLE_KEY!,
35
+ })
36
+ ```
37
+
38
+ Fetching brands then goes through `request`, with paths relative to `/api/v3/store`:
39
+
40
+ ```typescript
41
+ import type { PaginatedResponse } from '@spree/sdk'
42
+ import { client } from '@/lib/spree'
43
+ import type { Brand } from '@/types/brand'
44
+
45
+ const brands = await client.request<PaginatedResponse<Brand>>('GET', '/brands')
46
+ const wilson = await client.request<Brand>('GET', '/brands/brand_k5nR8xLq')
47
+ ```
48
+
49
+ Only the fields the Store serializer declared come back. Timestamps and anything admin-only are absent by design — that is the serializer split from step 1 doing its job.
50
+
51
+ > **NOTE:** A generated resource is addressed by its prefixed ID. Looking one up by slug needs a `find_resource` override on the controller — the products endpoint does exactly that, so `/products/{slug}` works. Until you add one for brands, route by ID and treat `slug` as display data.
52
+
53
+ ## Step 3: Expand brand on products
54
+
55
+ Because `Spree::Product` now belongs to a brand, product responses can carry it. Ask for it with `expand`:
56
+
57
+ ```typescript
58
+ // Without expand — the id only
59
+ const product = await client.products.get('prod_86Rf07xd4z')
60
+
61
+ // With expand — the nested brand object
62
+ const withBrand = await client.products.get('prod_86Rf07xd4z', {
63
+ expand: ['brand'],
64
+ })
65
+ ```
66
+
67
+ Expanding avoids a second round trip per product, which matters on a product listing page.
68
+
69
+ > **INFO:** For `expand: ['brand']` to work, the Product serializer must declare the association and the model must allowlist it for filtering. Add `brand` to the product's `whitelisted_ransackable_associations` if you also want `?q[brand_name_cont]=wilson`. Every allowlist entry is a query any caller can run, so add only what the storefront needs.
70
+
71
+ ## Step 4: Render it on the product page
72
+
73
+ In the Next.js storefront, fetch the product with its brand and render the name:
74
+
75
+ ```tsx apps/storefront/src/app/products/[slug]/page.tsx
76
+ import { client } from '@/lib/spree'
77
+
78
+ export default async function ProductPage({ params }: { params: Promise<{ slug: string }> }) {
79
+ const { slug } = await params
80
+ const product = await client.products.get(slug, { expand: ['brand'] })
81
+
82
+ return (
83
+ <article>
84
+ {product.brand && (
85
+ <a href={`/brands/${product.brand.id}`} className="text-sm text-neutral-500">
86
+ {product.brand.name}
87
+ </a>
88
+ )}
89
+ <h1>{product.name}</h1>
90
+ <p>{product.price.display}</p>
91
+ </article>
92
+ )
93
+ }
94
+ ```
95
+
96
+ And a brand page listing everything that brand makes:
97
+
98
+ ```tsx apps/storefront/src/app/brands/[id]/page.tsx
99
+ import { client } from '@/lib/spree'
100
+ import type { Brand } from '@/types/brand'
101
+
102
+ export default async function BrandPage({ params }: { params: Promise<{ id: string }> }) {
103
+ const { id } = await params
104
+ const brand = await client.request<Brand>('GET', `/brands/${id}`)
105
+ const products = await client.products.list({ brand_id_eq: brand.id })
106
+
107
+ return (
108
+ <section>
109
+ <h1>{brand.name}</h1>
110
+ <p>{products.meta.count} products</p>
111
+ <ul>
112
+ {products.data.map((product) => (
113
+ <li key={product.id}>
114
+ <a href={`/products/${product.slug}`}>{product.name}</a>
115
+ </li>
116
+ ))}
117
+ </ul>
118
+ </section>
119
+ )
120
+ }
121
+ ```
122
+
123
+ `brand_id_eq` is a Ransack predicate, available because `brand_id` is on the product's ransackable allowlist. See [search and filtering](../core-concepts/search-filtering.md) for the full predicate list.
124
+
125
+ > **NOTE:** The storefront runs on its own dev server. From the project root, `spree dev` starts the API and dashboard; start the storefront alongside them with `cd apps/storefront && pnpm dev`. It reads the API URL and publishable key from `apps/storefront/.env.local`.
126
+
127
+ ## Next step
128
+
129
+ Customers can see brands. Now let other systems react when they change: [Events](events.md).
@@ -1,13 +1,169 @@
1
1
  ---
2
2
  title: Testing
3
- description: Test the feature at every layer — model and request specs with RSpec, dashboard plugin tests with Vitest, and an end-to-end check with Playwright.
3
+ description: Test the Brands feature at every layer — model and API specs with RSpec, dashboard tests with Vitest, and one end-to-end Playwright pass.
4
4
  ---
5
5
 
6
- Each layer of the feature gets the test that suits it: fast Ruby specs for the model and both API endpoints, component tests for the plugin, and one end-to-end pass proving the whole path works.
6
+ Each layer gets the test that suits it: fast backend specs for the model and both APIs, component tests for the dashboard, and one end-to-end pass proving the whole path works.
7
7
 
8
- ## What this chapter covers
8
+ Your project arrives with testing already configured — RSpec, FactoryBot, and Spree's own test helpers, including every factory core ships. You can write a spec against `create(:product)` without setting anything up.
9
9
 
10
- - Model and factory setup
11
- - Request specs for the Admin and Store API endpoints
12
- - Dashboard plugin tests with Vitest
13
- - An end-to-end Playwright spec: create an FAQ in the dashboard, see it on the storefront
10
+ ## Step 1: Run what the generator wrote
11
+
12
+ The generator in step 1 already wrote a factory and a controller spec per API side, so this step is mostly filling in rather than starting over:
13
+
14
+ ```text
15
+ server/spec/factories/spree/brand_factory.rb
16
+ server/spec/controllers/spree/api/v3/store/brands_controller_spec.rb
17
+ server/spec/controllers/spree/api/v3/admin/brands_controller_spec.rb
18
+ ```
19
+
20
+ Run them:
21
+
22
+ ```bash
23
+ spree rspec spec/controllers/spree/api/v3/
24
+ ```
25
+
26
+ The generated factory is the starting point for every other spec — always build records through factories rather than creating them directly, so a change to the model does not mean editing fifty specs.
27
+
28
+ ## Step 2: Model specs
29
+
30
+ Test what you wrote, not what the framework already guarantees. A presence validation on a required column needs no spec; the association behavior you chose does:
31
+
32
+ ```ruby server/spec/models/spree/brand_spec.rb
33
+ RSpec.describe Spree::Brand, type: :model do
34
+ it 'nullifies the reference instead of deleting products' do
35
+ brand = create(:brand)
36
+ product = create(:product, brand: brand)
37
+
38
+ brand.destroy
39
+
40
+ expect(product.reload.brand_id).to be_nil
41
+ expect(product).to be_persisted
42
+ end
43
+ end
44
+ ```
45
+
46
+ Prefer `build` over `create` when a record does not need to hit the database — it is markedly faster across a suite.
47
+
48
+ ## Step 3: Controller specs
49
+
50
+ Controller specs are where edge cases belong: authorization, scoping, filtering, error shapes. Always `render_views`, so a broken serializer fails the spec rather than passing silently:
51
+
52
+ ```ruby server/spec/controllers/spree/api/v3/store/brands_controller_spec.rb
53
+ RSpec.describe Spree::Api::V3::Store::BrandsController, type: :controller do
54
+ render_views
55
+
56
+ it 'exposes public fields only' do
57
+ create(:brand, name: 'Wilson')
58
+
59
+ get :index, params: { format: :json }
60
+
61
+ record = json_response['data'].first
62
+ expect(record['id']).to start_with('brand_')
63
+ expect(record).not_to have_key('created_at')
64
+ end
65
+
66
+ it 'filters by name' do
67
+ create(:brand, name: 'Wilson')
68
+ create(:brand, name: 'Penn')
69
+
70
+ get :index, params: { q: { name_cont: 'wil' }, format: :json }
71
+
72
+ expect(json_response['data'].map { |b| b['name'] }).to eq(['Wilson'])
73
+ end
74
+ end
75
+ ```
76
+
77
+ That second example is worth keeping: it proves the Ransack allowlist from step 1 actually permits `name_cont`. Remove `name` from `whitelisted_ransackable_attributes` and this spec fails, which is exactly what you want.
78
+
79
+ > **NOTE:** Specs run against a store the suite sets up for you, available as `@default_store`. Use it rather than creating another store marked as the default — two of them in one run produce failures that surface far from their cause.
80
+
81
+ ## Step 4: Dashboard tests
82
+
83
+ The dashboard is a TypeScript app, so its code tests with Vitest:
84
+
85
+ ```bash
86
+ cd apps/dashboard && pnpm test
87
+ ```
88
+
89
+ Test the client wrappers and any logic in your pages. Mock the HTTP layer rather than the client module, so the test exercises the real request path:
90
+
91
+ ```ts apps/dashboard/src/brands/client.test.ts
92
+ import { http, HttpResponse } from 'msw'
93
+ import { setupServer } from 'msw/node'
94
+ import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'
95
+ import { BrandsClient } from './client'
96
+
97
+ const server = setupServer(
98
+ http.get('*/api/v3/admin/brands', () =>
99
+ HttpResponse.json({ data: [{ id: 'brand_k5nR8xLq', name: 'Wilson' }] }),
100
+ ),
101
+ )
102
+
103
+ beforeAll(() => server.listen())
104
+ afterEach(() => server.resetHandlers())
105
+ afterAll(() => server.close())
106
+
107
+ describe('BrandsClient', () => {
108
+ it('lists brands from the admin endpoint', async () => {
109
+ const result = await BrandsClient.list({ per_page: 10 })
110
+
111
+ expect(result.data).toHaveLength(1)
112
+ expect(result.data[0].name).toBe('Wilson')
113
+ })
114
+ })
115
+ ```
116
+
117
+ Mocking the HTTP layer rather than the client module means the test exercises the real request path — the URL, the params and the response shape — so a wrong endpoint fails here instead of in the browser.
118
+
119
+ ## Step 5: One end-to-end pass
120
+
121
+ Everything above tests a layer in isolation. One Playwright spec proves the layers are actually connected — a brand created through the Admin API reaches the dashboard screen you built:
122
+
123
+ ```ts apps/dashboard/e2e/brands.spec.ts
124
+ import { expect, test } from '@playwright/test'
125
+ import { createBrand } from './helpers/api'
126
+ import { login } from './helpers/login'
127
+
128
+ test('a brand appears on the brands screen', async ({ page }) => {
129
+ const name = `Wilson ${Date.now()}`
130
+ await createBrand({ name, slug: `wilson-${Date.now()}` })
131
+
132
+ await login(page)
133
+ await page.getByRole('link', { name: /brands/i }).click()
134
+
135
+ await expect(page.getByRole('cell', { name })).toBeVisible()
136
+ })
137
+ ```
138
+
139
+ Navigating from the sidebar rather than calling `page.goto('/brands')` keeps the spec honest about the real path, which is store-scoped (`/:storeId/brands`) and would otherwise have to be hardcoded.
140
+
141
+ Once you add a create form, replace the API call with the form itself — filling it and asserting the new row is the stronger test, because it covers the write path too.
142
+
143
+ Two conventions matter here:
144
+
145
+ - **Drive the UI, assert on the UI.** Fill labels, click buttons, check visible text. Avoid waiting on API responses — `expect(...).toBeVisible()` polls until the condition holds, which covers nearly every case and keeps the test from breaking when the API shape changes.
146
+ - **Suffix names with `Date.now()`.** Specs share a database, and a fixed name collides with whatever an earlier run left behind.
147
+
148
+ ## What to run before you push
149
+
150
+ Run the specs for what you changed rather than the whole suite:
151
+
152
+ ```bash
153
+ spree rspec spec/models/spree/brand_spec.rb
154
+ spree rspec spec/controllers/spree/api/v3/
155
+ ```
156
+
157
+ Pass a file, a directory, or a single example with a line number (`spec/models/spree/brand_spec.rb:12`). Keep the full run for continuous integration, where waiting costs you nothing.
158
+
159
+ ## You're done
160
+
161
+ You have built one feature through every layer of Spree:
162
+
163
+ - A model with a Store API and an Admin API, generated in one command
164
+ - A dashboard plugin giving staff a place to manage it
165
+ - Storefront pages rendering it through typed SDK calls
166
+ - Lifecycle events other systems react to
167
+ - Tests at each layer, plus one pass across all of them
168
+
169
+ From here: [customization reference](../customization/quickstart.md) for extending core models and workflows, [dashboard customization](../dashboard/customization/quickstart.md) for deeper plugin work, and [core concepts](../core-concepts/architecture.md) for how the rest of the platform fits together.
@@ -138,6 +138,22 @@ bundle exec rake spree:stock:recount_levels
138
138
 
139
139
  `spree_stock_levels` gains `reserved_count` (units held by checkouts in progress) and `incoming_count` (units on their way on a purchase order placed with a supplier or a transfer in transit). Both are kept by the workflows that change them and start at zero on an existing install; this recomputes them from active reservations and open documents and prints every level it corrected. Until it runs, the dashboard's Inventory page shows zero reserved and incoming for stock that predates the upgrade. Safe to re-run at any time.
140
140
 
141
+ ### Name who performed past actions
142
+
143
+ ```bash
144
+ bundle exec rake spree:upgrade:backfill_actor_types
145
+ ```
146
+
147
+ Orders, refunds, returns, exchanges, claims and stock receipts remember who
148
+ cancelled, approved, created or received them. In 6.0 that can be a member of
149
+ staff **or an API key**, so each id column — `canceler_id`, `approver_id`,
150
+ `created_by_id`, `refunder_id`, `received_by_id` — gained a type column beside
151
+ it (`canceler_type`, and so on) naming which kind of actor the id points at.
152
+
153
+ Rows written before the upgrade carry an id and an empty type; this task fills
154
+ the type in. Until it runs they still read as the staff member they always
155
+ were, with a deprecation warning. Safe to re-run at any time.
156
+
141
157
  ## The Cart/Order split
142
158
 
143
159
  The single biggest change. What used to be one `Spree::Order` living through checkout and beyond is now two models:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spree/docs",
3
- "version": "0.1.236",
3
+ "version": "0.1.238",
4
4
  "description": "Spree Commerce developer documentation for AI agents and local reference",
5
5
  "type": "module",
6
6
  "license": "CC-BY-4.0",
@@ -1,13 +0,0 @@
1
- ---
2
- title: Admin API
3
- description: Add a ProductFaq endpoint to the Admin API — resource controller conventions, an admin serializer, and a permission catalog entry.
4
- ---
5
-
6
- Staff manage FAQs through the Admin API — the same API the dashboard, integrations and scripts use. This chapter adds the endpoint following the conventions every Spree resource follows, which is what makes the dashboard plugin in the next chapter nearly free.
7
-
8
- ## What this chapter covers
9
-
10
- - The resource controller: full CRUD from one small controller declaring its model, serializer and permitted attributes
11
- - The admin serializer and prefixed IDs
12
- - Registering the resource in the permission catalog so roles and API key scopes cover it
13
- - Exercising the endpoint with the Admin CLI
@@ -1,14 +0,0 @@
1
- ---
2
- title: Dashboard Plugin
3
- description: Scaffold a dashboard plugin for ProductFaqs — a table, a form, and navigation, generated by the CLI and wired to the Admin API endpoint.
4
- ---
5
-
6
- With the Admin API in place, the back-office UI is a dashboard plugin: a table listing FAQs, a form to create and edit them, and a navigation entry — scaffolded by the CLI and customized from there.
7
-
8
- ## What this chapter covers
9
-
10
- - Scaffolding the plugin with the CLI
11
- - The resource table: columns, search and pagination against the Admin API
12
- - The form: fields, validation errors mapped from API responses
13
- - Registering navigation and permissions-aware visibility
14
- - Running it against your development store
@@ -1,13 +0,0 @@
1
- ---
2
- title: Model
3
- description: Generate the ProductFaq model — migration, associations, validations and store scoping — the foundation both tracks build on.
4
- ---
5
-
6
- Every feature starts with a model. This chapter generates `ProductFaq` — question, answer, position, attached to a product and scoped to the store.
7
-
8
- ## What this chapter covers
9
-
10
- - Generating the model and migration from the command line
11
- - Store scoping — why every store-owned record belongs to a store, and what that buys you
12
- - The association to products, validations, and ordering
13
- - Running the migration and creating a record in the console