@spree/docs 0.1.246 → 0.1.248
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.
- package/dist/developer/agentic/llm-docs.md +1 -1
- package/dist/developer/core-concepts/sellers.md +28 -5
- package/dist/developer/create-spree-app/quickstart.md +1 -1
- package/dist/developer/dashboard/customization/theming.md +161 -0
- package/dist/developer/dashboard/overview.md +1 -1
- package/dist/developer/deployment/background_jobs.md +3 -2
- package/dist/developer/deployment/emails.md +1 -1
- package/dist/developer/how-to/build-a-marketplace.md +750 -11
- package/dist/developer/storefront/nextjs/deployment.md +8 -6
- package/dist/developer/storefront/nextjs/emails.md +1 -1
- package/dist/developer/storefront/nextjs/quickstart.md +3 -3
- package/dist/developer/storefront/nextjs/testing.md +12 -12
- package/dist/developer/storefront/nextjs/wallet-payments.md +1 -1
- package/dist/developer/upgrades/5.6-to-6.0.md +0 -1
- package/package.json +1 -1
|
@@ -29,7 +29,7 @@ https://spreecommerce.org/docs/developer/core-concepts/orders.md
|
|
|
29
29
|
The full developer documentation also ships as an npm package, so agents working in your repository can `Read` doc files locally — no network round-trips, works offline, and version-pinned to what you installed:
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
|
-
|
|
32
|
+
pnpm add -D @spree/docs
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
```text
|
|
@@ -242,6 +242,25 @@ Two details worth knowing:
|
|
|
242
242
|
- **Delivery and order-level fees are shared out by item value**, so a seller whose goods made up most of the basket carries most of the delivery charge.
|
|
243
243
|
|
|
244
244
|
> **WARNING:** A storefront must handle the possibility of an order group. Completing a cart may yield one order or several, and a customer's order history should show the group as one purchase rather than confronting them with three orders they don't remember placing separately.
|
|
245
|
+
>
|
|
246
|
+
> A back office must handle it too: an order raised from the admin divides the same way, so completing one may answer with a group instead of an order.
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
```typescript Admin SDK
|
|
250
|
+
import { isOrderGroup } from '@spree/admin-sdk'
|
|
251
|
+
|
|
252
|
+
const result = await client.orders.complete('or_UkLWZg9DAJ')
|
|
253
|
+
|
|
254
|
+
// One order per seller when the order held several sellers' goods.
|
|
255
|
+
const orders = isOrderGroup(result) ? result.orders : [result]
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
```bash cURL
|
|
259
|
+
curl -X PATCH https://your-store.com/api/v3/admin/orders/or_UkLWZg9DAJ/complete \
|
|
260
|
+
-H "X-Spree-API-Key: sk_xxx" \
|
|
261
|
+
-H "Content-Type: application/json"
|
|
262
|
+
```
|
|
263
|
+
|
|
245
264
|
|
|
246
265
|
## Commission
|
|
247
266
|
|
|
@@ -308,18 +327,22 @@ The scheduler is the host app's. Two jobs fan out per seller and the interval lo
|
|
|
308
327
|
| `Spree::SellerPayouts::SweepDueJob` | daily | settles every seller who is due |
|
|
309
328
|
| `Spree::SellerTransfers::ExecutePendingDueJob` | hourly | retries earnings a provider refused — usually a passing thing |
|
|
310
329
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
330
|
+
A project created with `create-spree-app` schedules both already, in
|
|
331
|
+
`server/config/recurring.yml`. They are a cheap no-op on a store with no
|
|
332
|
+
sellers, so there is nothing to turn off if you are not running a marketplace.
|
|
333
|
+
|
|
334
|
+
> **WARNING:** On an existing project, or one using a scheduler of its own, check both are
|
|
335
|
+
> scheduled — a marketplace that never runs the sweep never pays a seller, and
|
|
336
|
+
> nothing reports the omission.
|
|
314
337
|
>
|
|
315
338
|
> ```yaml server/config/recurring.yml
|
|
316
339
|
production:
|
|
317
340
|
sweep_due_seller_payouts:
|
|
318
341
|
class: Spree::SellerPayouts::SweepDueJob
|
|
319
|
-
schedule: every day
|
|
342
|
+
schedule: at 3am every day
|
|
320
343
|
execute_pending_seller_transfers:
|
|
321
344
|
class: Spree::SellerTransfers::ExecutePendingDueJob
|
|
322
|
-
schedule: every hour
|
|
345
|
+
schedule: every hour at minute 27
|
|
323
346
|
```
|
|
324
347
|
|
|
325
348
|
A seller on the `manual` interval is skipped by the sweep and settled by hand instead:
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Theming
|
|
3
|
+
description: Restyle the dashboard by overriding its design tokens — colours, radius, fonts and the type scale — without forking a single component.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
The dashboard is styled entirely from CSS custom properties. Components read
|
|
7
|
+
tokens rather than hard-coded colours, so changing a token restyles everything
|
|
8
|
+
that uses it — you never fork a component to recolour it.
|
|
9
|
+
|
|
10
|
+
## Where your styles go
|
|
11
|
+
|
|
12
|
+
Your app's `src/styles.css` imports the Spree stylesheet. Anything below that
|
|
13
|
+
import wins, because it lands later at the same specificity:
|
|
14
|
+
|
|
15
|
+
```css src/styles.css
|
|
16
|
+
@import "@spree/dashboard/styles.css";
|
|
17
|
+
|
|
18
|
+
:root {
|
|
19
|
+
--primary: oklch(0.55 0.22 264);
|
|
20
|
+
--radius: 0.25rem;
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
That is the whole mechanism. No build config, no plugin, no component overrides.
|
|
25
|
+
|
|
26
|
+
> **NOTE:** The seller panel works the same way — its starter imports
|
|
27
|
+
> `@spree/seller-dashboard/styles.css`, and the same tokens apply.
|
|
28
|
+
|
|
29
|
+
## Dark mode
|
|
30
|
+
|
|
31
|
+
Every token has a dark counterpart under `.dark`. A theme is only complete when
|
|
32
|
+
you set both:
|
|
33
|
+
|
|
34
|
+
```css src/styles.css
|
|
35
|
+
@import "@spree/dashboard/styles.css";
|
|
36
|
+
|
|
37
|
+
:root {
|
|
38
|
+
--primary: oklch(0.55 0.22 264);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.dark {
|
|
42
|
+
--primary: oklch(0.72 0.19 264);
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
> **WARNING:** The theme provider decides *mode* — light, dark or system — and toggles a
|
|
47
|
+
> `.dark` class. It holds no colours. Override only `:root` and your theme
|
|
48
|
+
> breaks the moment someone switches to dark.
|
|
49
|
+
|
|
50
|
+
## The tokens
|
|
51
|
+
|
|
52
|
+
### Surfaces and text
|
|
53
|
+
|
|
54
|
+
| Token | What it colours |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `--background` / `--foreground` | The page, and text on it |
|
|
57
|
+
| `--card` / `--card-foreground` | Cards, and text on them |
|
|
58
|
+
| `--popover` / `--popover-foreground` | Menus, dropdowns, popovers |
|
|
59
|
+
| `--muted` / `--muted-foreground` | Recessed surfaces and secondary text |
|
|
60
|
+
|
|
61
|
+
### Brand and interaction
|
|
62
|
+
|
|
63
|
+
| Token | What it colours |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `--primary` / `--primary-foreground` | Primary buttons and calls to action |
|
|
66
|
+
| `--secondary` / `--secondary-foreground` | Secondary buttons |
|
|
67
|
+
| `--accent` / `--accent-foreground` | Hovered rows, selected items |
|
|
68
|
+
| `--accent-hover`, `--accent-strong`, `--accent-strong-hover` | Deeper accent steps |
|
|
69
|
+
| `--link` / `--link-hover` | Inline text links |
|
|
70
|
+
| `--ring` | Focus rings |
|
|
71
|
+
|
|
72
|
+
### Status
|
|
73
|
+
|
|
74
|
+
`--destructive` and `--destructive-foreground` for dangerous actions, plus four
|
|
75
|
+
status families used by badges and callouts — each with `-bg`, `-border` and
|
|
76
|
+
`-fg`:
|
|
77
|
+
|
|
78
|
+
`--status-green-*`, `--status-amber-*`, `--status-red-*`, `--status-blue-*`
|
|
79
|
+
|
|
80
|
+
### Borders and structure
|
|
81
|
+
|
|
82
|
+
| Token | What it colours |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `--border` | The default border, aliasing `--border-base` |
|
|
85
|
+
| `--border-card`, `--border-subtle`, `--border-control` | Cards, dividers, inputs |
|
|
86
|
+
| `--input` | Input borders |
|
|
87
|
+
| `--radius` | Corner radius everywhere |
|
|
88
|
+
|
|
89
|
+
### Sidebar
|
|
90
|
+
|
|
91
|
+
The sidebar has its own family so you can give the chrome a different tone from
|
|
92
|
+
the content: `--sidebar`, `--sidebar-foreground`, `--sidebar-primary`,
|
|
93
|
+
`--sidebar-accent` and its hover steps, `--sidebar-border`, `--sidebar-ring`.
|
|
94
|
+
|
|
95
|
+
### Charts
|
|
96
|
+
|
|
97
|
+
`--chart-1` through `--chart-5`, in series order.
|
|
98
|
+
|
|
99
|
+
## Tokens defined from other tokens
|
|
100
|
+
|
|
101
|
+
Several tokens alias another rather than naming their own colour:
|
|
102
|
+
|
|
103
|
+
```css
|
|
104
|
+
--primary: var(--foreground);
|
|
105
|
+
--popover: var(--card);
|
|
106
|
+
--accent: var(--secondary);
|
|
107
|
+
--muted: var(--background);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
So overriding one base token cascades. Setting `--foreground` alone also moves
|
|
111
|
+
primary buttons, because that is where `--primary` points. Override the alias
|
|
112
|
+
directly when you want them to diverge.
|
|
113
|
+
|
|
114
|
+
## Fonts and the type scale
|
|
115
|
+
|
|
116
|
+
Typography is set in a `@theme inline` block and overridden the same way:
|
|
117
|
+
|
|
118
|
+
```css src/styles.css
|
|
119
|
+
@import "@spree/dashboard/styles.css";
|
|
120
|
+
|
|
121
|
+
@theme inline {
|
|
122
|
+
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
|
123
|
+
--text-base: 0.9375rem;
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The dashboard's scale is deliberately smaller than Tailwind's default, because
|
|
128
|
+
admin screens are dense. Raising `--text-base` raises everything built on it.
|
|
129
|
+
|
|
130
|
+
## Using tokens in your own pages
|
|
131
|
+
|
|
132
|
+
Tailwind maps every token through `@theme inline`, so utilities follow your
|
|
133
|
+
values with no extra work:
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
<div className="bg-card text-card-foreground border-border rounded-lg border">
|
|
137
|
+
<p className="text-muted-foreground">Follows your theme.</p>
|
|
138
|
+
</div>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Use the utilities rather than raw `var(--token)` — a page built from
|
|
142
|
+
`bg-card` and `text-muted-foreground` inherits every future change, including
|
|
143
|
+
dark mode.
|
|
144
|
+
|
|
145
|
+
For inline text links there is a `.link` class carrying the canonical treatment:
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
<Link to="/$storeId/settings/store" params={{ storeId }} className="link">
|
|
149
|
+
Store settings
|
|
150
|
+
</Link>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
It is for links in prose and help text. A link that is really a table row, a
|
|
154
|
+
breadcrumb, a nav item or a button rendered as an anchor carries its own
|
|
155
|
+
styling, and `.link` would fight it.
|
|
156
|
+
|
|
157
|
+
## Related
|
|
158
|
+
|
|
159
|
+
- [Dashboard overview](../overview.md) — the three packages and what each is for
|
|
160
|
+
- [Translations](translations.md) — the other half of white-labelling
|
|
161
|
+
- [Slots](slots.md) — adding your own UI to existing screens
|
|
@@ -44,7 +44,7 @@ Your copy of the dashboard is a small Vite app (the "host app") that imports `<D
|
|
|
44
44
|
- **New project:** `npx create-spree-app my-store` — answer **Yes** to "Include React Dashboard?" and it lands in `apps/dashboard/`, pointed at your API.
|
|
45
45
|
- **Existing project:** run `npx spree add dashboard` from your project root — same result.
|
|
46
46
|
|
|
47
|
-
Either way: `cd apps/dashboard &&
|
|
47
|
+
Either way: `cd apps/dashboard && pnpm dev`, open http://localhost:5173, and sign in with your admin email and password. No API keys to configure — admins authenticate interactively.
|
|
48
48
|
|
|
49
49
|
## Choose your path
|
|
50
50
|
|
|
@@ -34,11 +34,12 @@ production:
|
|
|
34
34
|
|
|
35
35
|
### Marketplace payouts
|
|
36
36
|
|
|
37
|
-
A marketplace
|
|
37
|
+
A marketplace settles its sellers on a recurring job, which a new project
|
|
38
|
+
already schedules:
|
|
38
39
|
|
|
39
40
|
```yaml config/recurring.yml
|
|
40
41
|
production:
|
|
41
|
-
|
|
42
|
+
sweep_due_seller_payouts:
|
|
42
43
|
class: Spree::SellerPayouts::SweepDueJob
|
|
43
44
|
schedule: at 3am every day
|
|
44
45
|
```
|
|
@@ -141,7 +141,7 @@ When `SMTP_HOST` is not set, emails are printed to the Rails log instead of bein
|
|
|
141
141
|
In development, no email provider is needed. Emails are rendered to HTML files in `.next/emails/` with a clickable `file://` link in the console. To preview and design templates:
|
|
142
142
|
|
|
143
143
|
```bash
|
|
144
|
-
|
|
144
|
+
pnpm email:dev
|
|
145
145
|
```
|
|
146
146
|
|
|
147
147
|
To test the full webhook flow locally, use [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/):
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Build a Multi-Vendor Marketplace
|
|
3
|
-
description:
|
|
3
|
+
description: Learn how to extremely fast launch a marketplace on Spree
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
Spree fully supports multi-vendor marketplaces out of the box: sellers are first-class records, orders split per seller at completion, robust commission engine, and a payout ledger with pluggable payout providers.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
So building a marketplace doesn't require weeks or months of development, it's just configuring your Spree application to admit sellers, review their submissions, and pay them. In most cases you don't need to customize Spree or write any code at all. This guide is a walkthrough of all elements that make a marketplace, and how to configure them.
|
|
9
9
|
|
|
10
|
-
## What you
|
|
10
|
+
## What you get
|
|
11
11
|
|
|
12
12
|
- **Sellers** — onboard sellers with an operator-configured requirements checklist, review submissions, and approve them for selling.
|
|
13
13
|
- **Order splitting** — a customer checks out once; completion produces per-seller orders grouped under one purchase.
|
|
@@ -15,11 +15,750 @@ Spree brings multi-vendor marketplace support into the open-source core: sellers
|
|
|
15
15
|
- **Payouts** — a transfer and payout ledger, with Stripe Connect payouts (Express onboarding, on-fulfillment transfers) shipping in the open-source monorepo.
|
|
16
16
|
- **Seller operations** — sellers work through their own dedicated API surface, scoped so a seller only ever sees their own trade.
|
|
17
17
|
|
|
18
|
-
##
|
|
18
|
+
## Before you start
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
You need a running Spree 6 project and admin credentials. The marketplace surfaces are:
|
|
21
|
+
|
|
22
|
+
| Surface | Who uses it | Package |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| Admin dashboard | The marketplace operator | [`@spree/dashboard`](../dashboard/overview.md) |
|
|
25
|
+
| Seller panel | Each seller's own team | [`@spree/seller-dashboard`](#step-7-stand-up-the-seller-panel) |
|
|
26
|
+
| Admin API | Operator-side integrations | [`@spree/admin-sdk`](../../api-reference/admin-api/introduction.md) |
|
|
27
|
+
| Seller API | A signed-in seller only | [`@spree/seller-sdk`](../../api-reference/seller-api/introduction.md) |
|
|
28
|
+
|
|
29
|
+
To try the flow quickly, you can seed a sample seller with an owner account, a pending invitation and a fund ledger:
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
```bash Spree CLI
|
|
33
|
+
spree run rake spree:sellers:sample_data
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```bash Without CLI
|
|
37
|
+
bin/rails spree:sellers:sample_data
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
The task refuses to run outside development and test, because it writes a known password. It signs in at `seller@example.com` / `spree123` — override with `SELLER_EMAIL`, `SELLER_PASSWORD` and `SELLER_NAME`.
|
|
42
|
+
|
|
43
|
+
## Configure the store for marketplace trading
|
|
44
|
+
|
|
45
|
+
There is no single "marketplace mode" switch. A store becomes a marketplace when it has sellers; what you configure up front is how strictly it admits them and how it pays them.
|
|
46
|
+
|
|
47
|
+
### Payout settings
|
|
48
|
+
|
|
49
|
+
Three store preferences decide how sellers get paid. All three are writable through the Admin API, and they appear in the dashboard under **Settings → Marketplace**.
|
|
50
|
+
|
|
51
|
+
| Preference | Default | What it decides |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| `preferred_payout_provider` | blank | Who moves the money. Blank means core's record-only provider: the ledger is kept correctly and the operator settles by hand. |
|
|
54
|
+
| `preferred_default_payouts_schedule_interval` | `monthly` | How often sellers are settled. One of `daily`, `weekly`, `biweekly`, `monthly`, `manual`. A seller can carry its own interval, which wins. |
|
|
55
|
+
| `preferred_default_minimum_payout_amount` | `0` | What a seller's balance must reach before a settlement is worth sending. Below it the balance carries to the next period. |
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
```typescript Admin SDK
|
|
59
|
+
await adminClient.store.update({
|
|
60
|
+
preferred_default_payouts_schedule_interval: 'weekly',
|
|
61
|
+
preferred_default_minimum_payout_amount: 25,
|
|
62
|
+
})
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```bash cURL
|
|
66
|
+
curl -X PATCH 'https://api.mystore.com/api/v3/admin/store' \
|
|
67
|
+
-H 'X-Spree-API-Key: sk_xxx' \
|
|
68
|
+
-H 'Content-Type: application/json' \
|
|
69
|
+
-d '{
|
|
70
|
+
"preferred_default_payouts_schedule_interval": "weekly",
|
|
71
|
+
"preferred_default_minimum_payout_amount": 25
|
|
72
|
+
}'
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
A mistyped provider key is refused at write time rather than silently ignored, so you cannot end up with a bookkeeping-only ledger and no indication that your choice was dropped. Ask the API which providers are registered before you set one:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
curl 'https://api.mystore.com/api/v3/admin/payout_providers' \
|
|
80
|
+
-H 'X-Spree-API-Key: sk_xxx'
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Admission and review settings
|
|
84
|
+
|
|
85
|
+
Four more preferences decide who gets to trade and what they hear from you. They
|
|
86
|
+
sit on the same **Settings → Marketplace** screen, and are writable through the
|
|
87
|
+
Admin API like the payout ones.
|
|
88
|
+
|
|
89
|
+
| Preference | Default | What it decides |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `preferred_auto_approve_sellers` | `false` | Admit a seller the moment they finish the checklist, with nobody looking at them. |
|
|
92
|
+
| `preferred_auto_approve_seller_products` | `false` | Put a seller's product on sale the moment they submit it. |
|
|
93
|
+
| `preferred_send_seller_transactional_emails` | `true` | Whether Spree emails sellers. Turn it off if you front seller communications yourself. |
|
|
94
|
+
| `preferred_default_commission_tax_rate` | `0` | Tax on your commission when neither the rate nor the tax provider names one. |
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
```typescript Admin SDK
|
|
98
|
+
await adminClient.store.update({
|
|
99
|
+
preferred_auto_approve_sellers: false,
|
|
100
|
+
preferred_auto_approve_seller_products: false,
|
|
101
|
+
preferred_send_seller_transactional_emails: true,
|
|
102
|
+
preferred_default_commission_tax_rate: 0.23,
|
|
103
|
+
})
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```bash cURL
|
|
107
|
+
curl -X PATCH 'https://api.mystore.com/api/v3/admin/store' \
|
|
108
|
+
-H 'X-Spree-API-Key: sk_xxx' \
|
|
109
|
+
-H 'Content-Type: application/json' \
|
|
110
|
+
-d '{
|
|
111
|
+
"preferred_auto_approve_sellers": false,
|
|
112
|
+
"preferred_auto_approve_seller_products": false,
|
|
113
|
+
"preferred_send_seller_transactional_emails": true,
|
|
114
|
+
"preferred_default_commission_tax_rate": 0.23
|
|
115
|
+
}'
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
> **WARNING:** Turning either auto-approve preference on removes a human decision from your marketplace. `auto_approve_sellers` lets any applicant who ticks every box start trading; `auto_approve_seller_products` puts their listings on sale without review. Both are appropriate for an invite-only marketplace where you already trust the sellers, and dangerous for an open one.
|
|
120
|
+
|
|
121
|
+
Commission tax is stored as a fraction — `0.23` means 23%. The dashboard asks for
|
|
122
|
+
a percentage and converts, so you only meet the fraction through the API. See
|
|
123
|
+
[Tax on commission](../core-concepts/commissions.md#tax-on-commission) for
|
|
124
|
+
when it applies and what overrides it.
|
|
125
|
+
|
|
126
|
+
## Define what a seller must do before trading
|
|
127
|
+
|
|
128
|
+
A seller requirement is one thing the marketplace asks of a seller before it will let them trade. The operator composes the checklist from registered **kinds** — rows are configuration, and only a genuinely new kind of check needs code.
|
|
129
|
+
|
|
130
|
+
The checklist is enforced in exactly two places: when the seller asks to be reviewed, and when the operator approves them. Nothing else consults it.
|
|
131
|
+
|
|
132
|
+
### The kinds that ship
|
|
133
|
+
|
|
134
|
+
Thirteen kinds are registered by core. Seven of them are provisioned into a new store's default checklist, in the order a seller meets them.
|
|
135
|
+
|
|
136
|
+
| `type` | What it asks | Default? | Seller submits? | Operator reviews? |
|
|
137
|
+
|---|---|:---:|:---:|:---:|
|
|
138
|
+
| `accept_terms` | Read and accept the marketplace terms | ✅ | — | — |
|
|
139
|
+
| `complete_profile` | Fill in the public profile shoppers see | ✅ | — | — |
|
|
140
|
+
| `billing_address` | The address commission invoices are addressed to | ✅ | — | — |
|
|
141
|
+
| `returns_address` | Where customer returns are sent | ✅ | — | — |
|
|
142
|
+
| `delivery_method` | At least one way to ship | ✅ | — | — |
|
|
143
|
+
| `package_type` | The box they ship orders in, with measurements | ✅ | — | — |
|
|
144
|
+
| `minimum_products` | List at least this many products | ✅ | — | — |
|
|
145
|
+
| `payout_account` | An account the payout provider will pay | — | — | — |
|
|
146
|
+
| `required_custom_fields` | Named custom fields filled in (VAT number, registration number) | — | — | — |
|
|
147
|
+
| `policy` | Publish a policy with this name | — | — | — |
|
|
148
|
+
| `attestation` | Confirm something in their own right | — | ✅ | — |
|
|
149
|
+
| `operator_review` | Something a person at the marketplace checks by hand | — | ✅ | ✅ |
|
|
150
|
+
| `document` | Upload a file for the marketplace to review | — | ✅ | ✅ |
|
|
151
|
+
|
|
152
|
+
The first ten are **computed**: Spree reads the seller's own data and answers for itself, so there is nothing for the seller to submit. The last three take a **submission** — a row recording what the seller said and when — and the bottom two wait for someone to accept it.
|
|
153
|
+
|
|
154
|
+
Four kinds may be configured more than once per store, because their meaning comes from the operator's own wording: `attestation`, `operator_review`, `document` and `policy`. Those four require a `name`. The rest are one per store.
|
|
155
|
+
|
|
156
|
+
> **NOTE:** The generic kinds are deliberately absent from the default checklist. An attestation or a document means nothing until the operator has written what they are asking for, so Spree does not guess.
|
|
157
|
+
|
|
158
|
+
### Build the checklist from the registry, never a hardcoded list
|
|
159
|
+
|
|
160
|
+
Always read the kinds from `types()` rather than shipping your own list — a marketplace's own registered kinds then appear for free. The endpoint returns each kind's `preference_schema`, which is what a configuration form renders from.
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
```typescript Admin SDK
|
|
164
|
+
// What an operator can add, and the shape of each one's config form
|
|
165
|
+
const { data: kinds } = await adminClient.sellerRequirements.types()
|
|
166
|
+
|
|
167
|
+
// Ask every seller for a business registration
|
|
168
|
+
await adminClient.sellerRequirements.create({
|
|
169
|
+
type: 'document',
|
|
170
|
+
name: 'Business registration',
|
|
171
|
+
description: 'A certificate of incorporation or equivalent.',
|
|
172
|
+
required: true,
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
// Collect a VAT number through a custom field definition on Spree::Seller
|
|
176
|
+
await adminClient.sellerRequirements.create({
|
|
177
|
+
type: 'required_custom_fields',
|
|
178
|
+
custom_field_definition_ids: ['cfd_xxx'],
|
|
179
|
+
required: true,
|
|
180
|
+
})
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
```bash cURL
|
|
184
|
+
curl 'https://api.mystore.com/api/v3/admin/seller_requirements/types' \
|
|
185
|
+
-H 'X-Spree-API-Key: sk_xxx'
|
|
186
|
+
|
|
187
|
+
curl -X POST 'https://api.mystore.com/api/v3/admin/seller_requirements' \
|
|
188
|
+
-H 'X-Spree-API-Key: sk_xxx' \
|
|
189
|
+
-H 'Content-Type: application/json' \
|
|
190
|
+
-d '{
|
|
191
|
+
"type": "document",
|
|
192
|
+
"name": "Business registration",
|
|
193
|
+
"description": "A certificate of incorporation or equivalent.",
|
|
194
|
+
"required": true
|
|
195
|
+
}'
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
Each kind takes its own configuration through `preferences`. `accept_terms` carries `terms_body`, `terms_url` and `terms_effective_from` — setting that date is how you ask everyone again after rewriting the terms, since anyone who accepted before it falls back to unmet. `minimum_products` carries `minimum_count`. `complete_profile` carries `require_about`, `require_logo`, `require_cover_photo` and `require_contact_email`.
|
|
200
|
+
|
|
201
|
+
Two flags govern every row regardless of kind. `required: false` makes a line advisory — it appears on the seller's checklist but does not gate approval. `active: false` retires it without deleting the submissions that answered it. Position is the order sellers work through, and the list is drag-ordered in the dashboard.
|
|
202
|
+
|
|
203
|
+
> **NOTE:** `type` is write-once. The API strips it on update, because a saved row's submissions answered the old kind. To change a kind, delete the row and create a new one.
|
|
204
|
+
|
|
205
|
+
**Read more:** [Onboarding requirements](../core-concepts/sellers.md#onboarding-requirements) for the model, and [Seller Requirements](/user/settings/seller-requirements) for the operator's screen.
|
|
206
|
+
|
|
207
|
+
## Invite a seller and approve them
|
|
208
|
+
|
|
209
|
+
A seller moves through its life on the marketplace by **workflow**, never by assigning a status. Each transition is its own Admin API action because each carries its own arguments, sends its own mail and runs its own extension hooks — mass assignment would skip all three.
|
|
210
|
+
|
|
211
|
+
```mermaid
|
|
212
|
+
stateDiagram-v2
|
|
213
|
+
[*] --> pending : sellers.create
|
|
214
|
+
pending --> invited : sellers.invite
|
|
215
|
+
invited --> onboarding : invitation accepted
|
|
216
|
+
onboarding --> ready_for_review : seller submits
|
|
217
|
+
ready_for_review --> approved : sellers.approve
|
|
218
|
+
ready_for_review --> onboarding : sellers.reopenOnboarding
|
|
219
|
+
onboarding --> rejected : sellers.reject
|
|
220
|
+
ready_for_review --> rejected : sellers.reject
|
|
221
|
+
rejected --> approved : sellers.approve
|
|
222
|
+
approved --> suspended : sellers.suspend
|
|
223
|
+
suspended --> approved : sellers.approve
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
| Status | Meaning |
|
|
227
|
+
|---|---|
|
|
228
|
+
| `pending` | Created; nothing sent yet |
|
|
229
|
+
| `invited` | Someone has been asked to run it, and has not accepted |
|
|
230
|
+
| `onboarding` | Somebody accepted and is working through the checklist |
|
|
231
|
+
| `ready_for_review` | The seller says they are done; waiting on the operator |
|
|
232
|
+
| `approved` | Trading |
|
|
233
|
+
| `rejected` | An applicant turned away |
|
|
234
|
+
| `suspended` | A trading seller halted; reversible |
|
|
235
|
+
| `canceled` | Closed |
|
|
236
|
+
|
|
237
|
+
### Create and invite
|
|
238
|
+
|
|
239
|
+
Creating a seller through the workflow provisions a stock location for them, which is where their inventory lives and where customer returns land. A seller without one cannot finish onboarding, so this is not a step an operator can forget.
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
```typescript Admin SDK
|
|
243
|
+
const seller = await adminClient.sellers.create({
|
|
244
|
+
name: 'Bright Sparks',
|
|
245
|
+
contact_email: 'hello@brightsparks.example',
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
// Opens the seller's own team to someone; they join when they accept
|
|
249
|
+
await adminClient.sellers.invite(seller.id, { email: 'owner@brightsparks.example' })
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
```bash cURL
|
|
253
|
+
curl -X POST 'https://api.mystore.com/api/v3/admin/sellers' \
|
|
254
|
+
-H 'X-Spree-API-Key: sk_xxx' \
|
|
255
|
+
-H 'Content-Type: application/json' \
|
|
256
|
+
-d '{ "name": "Bright Sparks", "contact_email": "hello@brightsparks.example" }'
|
|
257
|
+
|
|
258
|
+
curl -X POST 'https://api.mystore.com/api/v3/admin/sellers/sel_xxx/invite' \
|
|
259
|
+
-H 'X-Spree-API-Key: sk_xxx' \
|
|
260
|
+
-H 'Content-Type: application/json' \
|
|
261
|
+
-d '{ "email": "owner@brightsparks.example" }'
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
The invited person accepts through the seller panel — the emailed link carries a prefixed ID and a token, which together are the credential. Accepting creates the membership and starts onboarding, so the seller reaches `onboarding` without the operator doing anything more.
|
|
266
|
+
|
|
267
|
+
Re-inviting is deliberate: invitations expire, and the first one goes to the wrong address often enough. What cannot be re-opened is a seller already trading or already turned away.
|
|
268
|
+
|
|
269
|
+
`role_id` names a role the seller owns; leave it out and they accept into the seller's own admin role. A seller's roles are the same permission machinery the store's back office uses, pointed at the seller — see [Staff & Roles](../core-concepts/staff-roles.md).
|
|
270
|
+
|
|
271
|
+
### Review submissions
|
|
272
|
+
|
|
273
|
+
The two reviewed kinds (`operator_review`, `document`) leave a submission for someone to decide. Accepting one satisfies that line of the checklist; rejecting sends it back with a note the seller reads.
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
```typescript Admin SDK
|
|
277
|
+
const { data: submissions } = await adminClient.sellers.requirementSubmissions.list('sel_xxx')
|
|
278
|
+
|
|
279
|
+
await adminClient.sellers.requirementSubmissions.accept('sel_xxx', 'selsub_xxx')
|
|
280
|
+
|
|
281
|
+
await adminClient.sellers.requirementSubmissions.reject('sel_xxx', 'selsub_xxx', {
|
|
282
|
+
review_note: 'The certificate has expired — please upload a current one.',
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
// Excuse this seller from something the store asks of everyone
|
|
286
|
+
await adminClient.sellers.requirementSubmissions.waive('sel_xxx', {
|
|
287
|
+
requirement_id: 'selreq_xxx',
|
|
288
|
+
review_note: 'Verified against Companies House by hand.',
|
|
289
|
+
})
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
```bash cURL
|
|
293
|
+
curl -X PATCH 'https://api.mystore.com/api/v3/admin/sellers/sel_xxx/requirement_submissions/selsub_xxx/reject' \
|
|
294
|
+
-H 'X-Spree-API-Key: sk_xxx' \
|
|
295
|
+
-H 'Content-Type: application/json' \
|
|
296
|
+
-d '{ "review_note": "The certificate has expired — please upload a current one." }'
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
A submission is `pending`, `accepted`, `rejected` or `waived`. A waiver is the operator recording that they dealt with this outside the marketplace — it reads as met without pretending the seller did it. Submissions accumulate rather than overwrite: the latest row for a (seller, requirement) pair decides the standing, and the ones before it are the record of how you got there.
|
|
301
|
+
|
|
302
|
+
Uploaded documents are stored privately and served only through the admin and seller branches. They are business registrations and identity documents, and Spree identifies them from their bytes rather than trusting the uploaded filename — a script named `certificate.pdf` is refused.
|
|
303
|
+
|
|
304
|
+
### Approve, suspend, reject
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
```typescript Admin SDK
|
|
308
|
+
// Where this seller stands against the checklist
|
|
309
|
+
const onboarding = await adminClient.sellers.onboarding('sel_xxx')
|
|
310
|
+
// → { status, progress: { done, total }, requirements: [...] }
|
|
311
|
+
|
|
312
|
+
await adminClient.sellers.approve('sel_xxx')
|
|
313
|
+
|
|
314
|
+
// Admit them anyway, despite an unfinished checklist
|
|
315
|
+
await adminClient.sellers.approve('sel_xxx', { override_requirements: true })
|
|
316
|
+
|
|
317
|
+
// Send them back to fix something
|
|
318
|
+
await adminClient.sellers.reopenOnboarding('sel_xxx', {
|
|
319
|
+
note: 'Your returns address is a PO box — we need a street address.',
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
await adminClient.sellers.suspend('sel_xxx', { reason: 'Unresolved delivery complaints' })
|
|
323
|
+
await adminClient.sellers.reject('sel_xxx', { reason: 'Outside our categories' })
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
```bash cURL
|
|
327
|
+
curl -X PATCH 'https://api.mystore.com/api/v3/admin/sellers/sel_xxx/approve' \
|
|
328
|
+
-H 'X-Spree-API-Key: sk_xxx' \
|
|
329
|
+
-H 'Content-Type: application/json' \
|
|
330
|
+
-d '{ "override_requirements": true }'
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
Approval is refused while a required requirement is unmet, unless `override_requirements` says the operator means it. The override is recorded on the `seller.approved` event along with what was outstanding — the operator can step over the checklist, but not without saying so.
|
|
335
|
+
|
|
336
|
+
**Suspend and reject are not the same thing.** Reject turns away an applicant who never traded; it refuses an approved seller outright. Suspend halts a trading seller: their catalog stops selling, their record and history stay, and `approve` is the way back. A suspended seller being reinstated is not re-measured against the checklist, because they were admitted once already.
|
|
337
|
+
|
|
338
|
+
A seller can also take themselves off sale without any operator involvement, by setting `holiday_mode_until`. The catalog stays visible; what stops is selling.
|
|
339
|
+
|
|
340
|
+
**Read more:** [The seller lifecycle](../core-concepts/sellers.md#the-seller-lifecycle), [Onboarding](/user/sellers/onboarding), [Managing sellers](/user/sellers/managing-sellers).
|
|
341
|
+
|
|
342
|
+
## Review what sellers list
|
|
343
|
+
|
|
344
|
+
Products belong to sellers through `seller_id` on `Spree::Product`. A product with no seller is the marketplace's own.
|
|
345
|
+
|
|
346
|
+
A marketplace adds two statuses to the product lifecycle that a single-merchant store never sees: `proposed` and `rejected`. A seller never assigns a status — they ask, and the marketplace decides.
|
|
347
|
+
|
|
348
|
+
```mermaid
|
|
349
|
+
stateDiagram-v2
|
|
350
|
+
draft --> proposed : seller submits
|
|
351
|
+
proposed --> active : operator approves
|
|
352
|
+
proposed --> rejected : operator rejects
|
|
353
|
+
rejected --> proposed : seller resubmits
|
|
354
|
+
active --> draft : seller takes it down
|
|
355
|
+
active --> archived : seller withdraws it
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
| Status | Meaning | Storefront |
|
|
359
|
+
|---|---|:---:|
|
|
360
|
+
| `draft` | The seller is still working on it | Hidden |
|
|
361
|
+
| `proposed` | Submitted, waiting on the marketplace | Hidden |
|
|
362
|
+
| `rejected` | Sent back with a reason, awaiting changes | Hidden |
|
|
363
|
+
| `active` | Approved and on sale | **Visible** |
|
|
364
|
+
| `archived` | Withdrawn | Hidden |
|
|
365
|
+
|
|
366
|
+
A seller can always take their own listing down — that is not a review decision. Putting one up is.
|
|
367
|
+
|
|
368
|
+
### How it relates to the submission record
|
|
369
|
+
|
|
370
|
+
The product's `status` is the operational truth; a `Spree::ProductSubmission` row is the record of how it got there — who asked, who decided, when, and what they told the seller. Its statuses are `pending`, `approved`, `rejected` and `withdrawn`.
|
|
371
|
+
|
|
372
|
+
Rows accumulate: a seller sent back three times leaves three rows, and the latest one is live. Taking a listing back to `draft` before anyone ruled on it closes the open row as `withdrawn`, so a `pending` row always means the marketplace still owes an answer rather than "abandoned".
|
|
373
|
+
|
|
374
|
+
> **WARNING:** A rejection reason belongs on the submission, never on the product. A seller can write their own product's `metadata`, so a note kept there is erased the next time they save. Send it as `reason` to the reject endpoint and Spree puts it where the seller cannot overwrite it.
|
|
375
|
+
|
|
376
|
+
### Both sides of the review
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
```typescript Seller SDK
|
|
380
|
+
// The seller asks for their listing to go on sale
|
|
381
|
+
await sellerClient.products.submit(productId)
|
|
382
|
+
|
|
383
|
+
// Or a selection of them
|
|
384
|
+
await sellerClient.products.bulkSubmit({ ids: [id1, id2] })
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
```typescript Admin SDK
|
|
388
|
+
// The operator decides
|
|
389
|
+
await adminClient.products.approve('prod_xxx')
|
|
390
|
+
await adminClient.products.reject('prod_xxx', {
|
|
391
|
+
reason: 'The lead photo has a competitor watermark.',
|
|
392
|
+
})
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
```bash cURL
|
|
396
|
+
curl -X PATCH 'https://api.mystore.com/api/v3/admin/products/prod_xxx/reject' \
|
|
397
|
+
-H 'X-Spree-API-Key: sk_xxx' \
|
|
398
|
+
-H 'Content-Type: application/json' \
|
|
399
|
+
-d '{ "reason": "The lead photo has a competitor watermark." }'
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
Filter the review queue with a Ransack query on status: `adminClient.products.list({ status_eq: 'proposed' })`.
|
|
404
|
+
|
|
405
|
+
With `preferred_auto_approve_seller_products` on, submitting chains straight into approval. The submission row still gets written and carries an `auto_approved` marker, so a blank reviewer reads as "this store does not review listings" rather than as a lost name.
|
|
406
|
+
|
|
407
|
+
There is no bulk route onto `active` for sellers, and that is deliberate: reaching it is the operator's decision on one listing at a time.
|
|
408
|
+
|
|
409
|
+
**Read more:** [Seller submissions](../core-concepts/products.md#seller-submissions), [Seller products](/user/sellers/products).
|
|
410
|
+
|
|
411
|
+
## Set what the marketplace charges
|
|
412
|
+
|
|
413
|
+
Commission is what the marketplace takes from a sale. You configure **rates**; Spree records what was actually charged as immutable **commission lines**, frozen when the order was placed.
|
|
414
|
+
|
|
415
|
+
### Rates and precedence
|
|
416
|
+
|
|
417
|
+
**The list is the precedence.** Rates are walked top-down and the first whose targeting matches the sale wins. A rate with no rules matches every sale, so anything below it is unreachable — the marketplace default belongs at the bottom of the list. A new rate is placed at the top by default, ahead of anything more general already there.
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
```typescript Admin SDK
|
|
421
|
+
// Discover the rule kinds this marketplace has registered
|
|
422
|
+
const { data: ruleTypes } = await adminClient.commissionRates.ruleTypes()
|
|
423
|
+
|
|
424
|
+
// 8% on one seller's electronics, with a floor and a cap per currency
|
|
425
|
+
await adminClient.commissionRates.create({
|
|
426
|
+
name: 'Electronics — Bright Sparks',
|
|
427
|
+
kind: 'percentage',
|
|
428
|
+
value: 8,
|
|
429
|
+
rules: [
|
|
430
|
+
{ type: 'seller_rule', preferences: { seller_ids: ['sel_xxx'] } },
|
|
431
|
+
],
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
// The marketplace default, at the bottom, matching everything
|
|
435
|
+
await adminClient.commissionRates.create({
|
|
436
|
+
name: 'Standard commission',
|
|
437
|
+
kind: 'percentage',
|
|
438
|
+
value: 15,
|
|
439
|
+
position: 99,
|
|
440
|
+
})
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
```bash cURL
|
|
444
|
+
curl -X POST 'https://api.mystore.com/api/v3/admin/commission_rates' \
|
|
445
|
+
-H 'X-Spree-API-Key: sk_xxx' \
|
|
446
|
+
-H 'Content-Type: application/json' \
|
|
447
|
+
-d '{ "name": "Standard commission", "kind": "percentage", "value": 15, "position": 99 }'
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
A rate is either `percentage` (a share of the sale) or `fixed` (a flat fee). A flat fee states its `amounts` per currency — a rate is skipped for a currency it names no amount in, so that sale falls through to the next matching rate rather than being charged a converted figure nobody set.
|
|
452
|
+
|
|
453
|
+
Rules ride the regular payload as `rules: [...]`, and the server replaces the rate's rules with exactly what it is sent. Every rule must hold for the rate to apply, and a rule naming several records means any of them — so "(Cameras OR Audio) AND that seller" is a category rule holding two ids beside a seller rule holding one.
|
|
454
|
+
|
|
455
|
+
### Lines are read-only
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
```typescript Admin SDK
|
|
459
|
+
const { data: lines } = await adminClient.commissionLines.list({ seller_id_eq: 'sel_xxx' })
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
```bash cURL
|
|
463
|
+
curl 'https://api.mystore.com/api/v3/admin/commission_lines?q[seller_id_eq]=sel_xxx' \
|
|
464
|
+
-H 'X-Spree-API-Key: sk_xxx'
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
There is no write path. Correcting a charge is a reversal, not an edit.
|
|
469
|
+
|
|
470
|
+
In the EU the fee is a separate supply from the sale, so it is taxed separately — that is what `preferred_default_commission_tax_rate` from step 1 is for, and a rate or a tax provider can name its own.
|
|
471
|
+
|
|
472
|
+
**Read more:** [Commissions](../core-concepts/commissions.md) covers the four rule types, gross-versus-net, how delivery is treated, currency floors and caps, and commission tax in full. [Commission rates](/user/sellers/commission-rates) is the operator's screen.
|
|
473
|
+
|
|
474
|
+
## Understand what a split checkout produces
|
|
475
|
+
|
|
476
|
+
This is the part that makes a marketplace different from a shop, and a storefront has to handle it.
|
|
477
|
+
|
|
478
|
+
A customer fills one basket, enters one address, and pays once. But each seller needs their own order — they fulfil separately, get paid separately, and must never see each other's business. So at completion, a checkout spanning several sellers becomes an **order group**: one container holding one order per seller.
|
|
479
|
+
|
|
480
|
+
```mermaid
|
|
481
|
+
erDiagram
|
|
482
|
+
OrderGroup ||--o{ Order : "one per seller"
|
|
483
|
+
OrderGroup ||--o{ Payment : "one payment"
|
|
484
|
+
Payment ||--o{ PaymentSplit : "apportioned"
|
|
485
|
+
Order }o--|| Seller : "belongs to"
|
|
486
|
+
Order ||--o{ Fulfillment : "shipped by that seller"
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
| Level | Owns |
|
|
490
|
+
|---|---|
|
|
491
|
+
| **Order group** | The customer, the addresses, the payment, the combined totals |
|
|
492
|
+
| **Order** | One seller's items, fulfillments and money lines |
|
|
493
|
+
|
|
494
|
+
The single payment is apportioned across the child orders as **payment splits**, so each seller's share of one charge is recorded exactly — which is what makes per-seller refunds and settlement possible later.
|
|
495
|
+
|
|
496
|
+
Two details worth knowing:
|
|
497
|
+
|
|
498
|
+
- **Group totals are added up, not divided.** The group's total is the sum of its children, so it always agrees with them.
|
|
499
|
+
- **Delivery and order-level fees are shared out by item value**, so a seller whose goods made up most of the basket carries most of the delivery charge.
|
|
500
|
+
|
|
501
|
+
> **WARNING:** **A storefront must handle the possibility of an order group.** Completing a cart yields either one order or a group, depending on how many sellers the basket reached. A customer's order history should show the group as one purchase rather than confronting them with three orders they don't remember placing separately.
|
|
502
|
+
|
|
503
|
+
A single-seller checkout produces no group at all — just the order, exactly as a normal store does. Nothing about a marketplace changes the shape of a single-seller sale.
|
|
504
|
+
|
|
505
|
+
Operators can read groups back; there is no write path, because everything an operator acts on lives on the orders inside them.
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
```typescript Admin SDK
|
|
509
|
+
const { data: groups } = await adminClient.orderGroups.list()
|
|
510
|
+
const group = await adminClient.orderGroups.get('ogrp_xxx', { expand: ['orders'] })
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
```bash cURL
|
|
514
|
+
curl 'https://api.mystore.com/api/v3/admin/order_groups/ogrp_xxx?expand=orders' \
|
|
515
|
+
-H 'X-Spree-API-Key: sk_xxx'
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
**Read more:** [One checkout, several sellers](../core-concepts/sellers.md#one-checkout-several-sellers), [Orders](../core-concepts/orders.md).
|
|
520
|
+
|
|
521
|
+
## Pay your sellers
|
|
522
|
+
|
|
523
|
+
The fund ledger has two levels, and both are written by fulfilment and by the scheduled sweep rather than by a caller.
|
|
524
|
+
|
|
525
|
+
| Level | Model | Written when |
|
|
526
|
+
|---|---|---|
|
|
527
|
+
| **Transfer** | `Spree::SellerTransfer` | One order earned one seller something — credited as the goods went out |
|
|
528
|
+
| **Payout** | `Spree::SellerPayout` | A settlement was swept up and sent |
|
|
529
|
+
|
|
530
|
+
A seller's balance is derived from the two rather than stored, per currency, because nothing is ever converted: a seller trading in two currencies accrues two balances and is paid twice. Only settlements known to have completed count against the balance, so one whose outcome was never established still reads as owed — the honest answer while nobody knows, and it cannot be paid twice by mistake.
|
|
531
|
+
|
|
532
|
+
### The sweep jobs
|
|
533
|
+
|
|
534
|
+
Two recurring jobs keep the ledger moving. A project created with
|
|
535
|
+
`create-spree-app` schedules both already:
|
|
536
|
+
|
|
537
|
+
```yaml server/config/recurring.yml
|
|
538
|
+
production:
|
|
539
|
+
sweep_due_seller_payouts:
|
|
540
|
+
class: Spree::SellerPayouts::SweepDueJob
|
|
541
|
+
schedule: at 3am every day
|
|
542
|
+
execute_pending_seller_transfers:
|
|
543
|
+
class: Spree::SellerTransfers::ExecutePendingDueJob
|
|
544
|
+
schedule: every hour at minute 27
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
> **WARNING:** On an existing project, or one running a scheduler of its own, confirm both are
|
|
548
|
+
> scheduled. A marketplace that never runs the sweep never pays a seller, and
|
|
549
|
+
> nothing reports the omission.
|
|
550
|
+
|
|
551
|
+
| Job | Run it | Does |
|
|
552
|
+
|---|---|---|
|
|
553
|
+
| `Spree::SellerPayouts::SweepDueJob` | daily | settles every seller who is due |
|
|
554
|
+
| `Spree::SellerTransfers::ExecutePendingDueJob` | hourly | retries earnings a provider refused — usually a passing thing |
|
|
555
|
+
|
|
556
|
+
### Settling by hand
|
|
557
|
+
|
|
558
|
+
A seller on the `manual` interval is skipped by the sweep. That is what the interval means: the operator decides when. The same endpoint also pays any seller early.
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
```typescript Admin SDK
|
|
562
|
+
// Where the seller stands, one row per currency
|
|
563
|
+
const { data: balances } = await adminClient.sellers.balances('sel_xxx')
|
|
564
|
+
|
|
565
|
+
// Settle them now, whatever their schedule says — 422 if there is nothing owed
|
|
566
|
+
await adminClient.sellers.settle('sel_xxx')
|
|
567
|
+
|
|
568
|
+
// The built-in provider: record that the bank transfer went out
|
|
569
|
+
await adminClient.sellerPayouts.complete('vpo_xxx', { reference: 'TRN-2026-0912-0042' })
|
|
570
|
+
|
|
571
|
+
// Read the ledger
|
|
572
|
+
const { data: transfers } = await adminClient.sellerTransfers.list({ seller_id_eq: 'sel_xxx' })
|
|
573
|
+
const { data: payouts } = await adminClient.sellerPayouts.list({ seller_id_eq: 'sel_xxx' })
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
```bash cURL
|
|
577
|
+
curl 'https://api.mystore.com/api/v3/admin/sellers/sel_xxx/balances' \
|
|
578
|
+
-H 'X-Spree-API-Key: sk_xxx'
|
|
579
|
+
|
|
580
|
+
curl -X POST 'https://api.mystore.com/api/v3/admin/sellers/sel_xxx/payouts' \
|
|
581
|
+
-H 'X-Spree-API-Key: sk_xxx'
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
### Payout providers
|
|
586
|
+
|
|
587
|
+
A provider is a stateless class registered in `Spree.payout_providers` and chosen per store with `preferred_payout_provider`.
|
|
588
|
+
|
|
589
|
+
| Provider | Moves money? | Completion |
|
|
590
|
+
|---|---|---|
|
|
591
|
+
| `Spree::PayoutProvider::System` (default) | No — keeps the books | The operator marks each payout paid, with the bank reference |
|
|
592
|
+
| Stripe Connect (`spree_stripe`) | Yes — transfers each earning to the seller's connected account as goods ship | A Stripe webhook, when the money lands |
|
|
593
|
+
|
|
594
|
+
Providers that need a seller to hold an account with them answer `requires_payout_account?` and implement `onboarding_url` — which is what the `payout_account` requirement from step 2 drives. Those links are short-lived and single-use, so the seller panel asks for a fresh one at the moment of clicking rather than minting one while drawing the checklist.
|
|
595
|
+
|
|
596
|
+
A connected provider confirms settlements through `POST /api/v3/webhooks/payouts/:payment_method_id` — separate from the payment webhook, because providers scope seller-account events to their own subscription and signing secret.
|
|
597
|
+
|
|
598
|
+
**Read more:** [Payouts](../core-concepts/sellers.md#payouts) for the ledger, [Stripe Connect for marketplaces](../../integrations/payments/stripe-connect.md) for the shipped provider, [Seller payouts](../providers/payouts.md) to write your own, and [Payouts settings](/user/settings/marketplace) for the operator's screen.
|
|
599
|
+
|
|
600
|
+
> **NOTE:** Refund clawbacks and netting across settlements, reconciliation, KYC operations and seller tax reporting (DAC7) are Spree Enterprise.
|
|
601
|
+
|
|
602
|
+
## Stand up the seller panel
|
|
603
|
+
|
|
604
|
+
Sellers get their own application — never access to the marketplace's dashboard. It is a separate React SPA against a separate API, shipped as `@spree/seller-dashboard`.
|
|
605
|
+
|
|
606
|
+
It shares its foundations with the admin dashboard: components from `@spree/dashboard-ui`, framework pieces (registries, providers, hooks) from `@spree/dashboard-core`. The two panels therefore stay visually and behaviourally consistent, and anything you learn customizing one applies to the other.
|
|
607
|
+
|
|
608
|
+
### Scaffold and run it
|
|
609
|
+
|
|
610
|
+
The panel ships as a shell you render from a thin host app that you own. Two ways to get one:
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
```bash New project
|
|
614
|
+
npx create-spree-app my-marketplace
|
|
615
|
+
# Both panels are scaffolded automatically — no prompt to answer
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
```bash Existing project
|
|
619
|
+
npx spree add seller-dashboard
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
A new project always gets both admin SPAs, so the starter's Docker image bakes both in; a store that never invites a seller simply leaves `/sellers` unvisited. Either way the host app lands at `apps/seller-dashboard/` with its own `package.json`, Vite config and a `src/plugins.ts` for your customizations. Then:
|
|
624
|
+
|
|
625
|
+
```bash
|
|
626
|
+
cd apps/seller-dashboard && pnpm dev
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
It serves on port **5174** (the admin dashboard is on 5173, so both run side by side).
|
|
630
|
+
|
|
631
|
+
`.env.local` holds one setting — the Rails server the dev proxy forwards `/api` to:
|
|
632
|
+
|
|
633
|
+
```bash apps/seller-dashboard/.env.local
|
|
634
|
+
VITE_API_PROXY_TARGET=http://localhost:3000
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
> **WARNING:** Do **not** set `VITE_SPREE_API_URL` in development. It switches the SDK to absolute cross-origin URLs and bypasses the proxy, which breaks the seller refresh-token cookie: the cookie rides under `SameSite=Lax`, and that only works while the panel is same-origin with the API.
|
|
638
|
+
|
|
639
|
+
### What sellers get out of the box
|
|
640
|
+
|
|
641
|
+
| Area | Pages |
|
|
642
|
+
|---|---|
|
|
643
|
+
| Onboarding | The checklist, submissions, payout-account link |
|
|
644
|
+
| Products | List, create, edit, submit for review |
|
|
645
|
+
| Orders | List, detail, fulfillments, deliveries, labels, returns, exchanges, claims |
|
|
646
|
+
| Money | Earnings, payouts |
|
|
647
|
+
| Profile | Public profile, tax identifiers |
|
|
648
|
+
| Settings | Team, delivery methods, package types, stock locations, policies |
|
|
649
|
+
| Auth | Login, forgot/reset password, invitation acceptance |
|
|
650
|
+
|
|
651
|
+
Sellers cannot see other sellers, the marketplace's own catalog, or anything belonging to the store at large.
|
|
652
|
+
|
|
653
|
+
### Deploying it
|
|
654
|
+
|
|
655
|
+
By default the seller panel, same as dashboard is build with Spree docker image and is server via `/sellers` path. You can also deploy it as a static bundle on a CDN or static host. The entire process is baked into the default `Dockerfile` which you get when you scaffold a new project with `create-spree-app`. The `spree build --production` command builds both panels and bakes them into the image, so you can run it as-is. This also automatically resolves CORS issues, since the panel is served from the same origin as the API.
|
|
656
|
+
|
|
657
|
+
### Customizing it
|
|
658
|
+
|
|
659
|
+
`src/plugins.ts` in your host app is where customizations are registered — the same `defineDashboardPlugin` API the admin dashboard uses, because both panels sit on `@spree/dashboard-core`.
|
|
660
|
+
|
|
661
|
+
```tsx apps/seller-dashboard/src/plugins.ts
|
|
662
|
+
import { defineDashboardPlugin } from '@spree/seller-dashboard'
|
|
663
|
+
import { PayoutsPage } from './pages/payouts'
|
|
664
|
+
|
|
665
|
+
defineDashboardPlugin({
|
|
666
|
+
nav: {
|
|
667
|
+
add: [{ key: 'payouts', label: 'Payouts', path: '/payouts', position: 300 }],
|
|
668
|
+
remove: ['team'],
|
|
669
|
+
},
|
|
670
|
+
slots: {
|
|
671
|
+
'seller.team.after': [{ id: 'audit', component: TeamAuditCard }],
|
|
672
|
+
},
|
|
673
|
+
})
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
Import everything from `@spree/seller-dashboard` — it re-exports both the framework and the design system, so you never have to work out which package an export lives in.
|
|
677
|
+
|
|
678
|
+
| What you want | How |
|
|
679
|
+
|---|---|
|
|
680
|
+
| Add or remove a nav entry | `nav.add` / `nav.remove` in `defineDashboardPlugin` |
|
|
681
|
+
| Inject a widget into a built-in page | A [slot](../dashboard/customization/slots.md) |
|
|
682
|
+
| Add a page | A [route](../dashboard/customization/routes.md) plus a nav entry |
|
|
683
|
+
| Add a table column | The [table registry](../dashboard/customization/tables.md) |
|
|
684
|
+
| Change wording | [Translations](../dashboard/customization/translations.md) merged with `i18n.addResourceBundle` |
|
|
685
|
+
| Install a third-party plugin | `npm add <plugin>` and restart — the Vite plugin picks it up from your dependencies |
|
|
686
|
+
|
|
687
|
+
The seller panel currently exposes just two slots, both on its team page: `seller.team.actions` and `seller.team.after`. (`seller.form_main` and `seller.form_sidebar` sound related but belong to the *admin* dashboard's seller detail page — they are for the operator's view of a seller, not the seller's own.) See the [slots catalog](../dashboard/slots-catalog.md) for their contexts. To put a widget anywhere else in the panel, add a route rather than looking for a slot that is not there yet.
|
|
688
|
+
|
|
689
|
+
> **NOTE:** **White-labelling is theming and copy, not a branding config object.** There is no logo or brand-name setting to fill in. You restyle by owning the Tailwind layer in your host app, and you re-word by overriding translation keys. Both are real customization paths — there is simply no shortcut that skips them.
|
|
690
|
+
|
|
691
|
+
> **WARNING:** **`@spree/seller-dashboard` ships English only.** The admin dashboard ships several locales; the seller panel ships `en.json` and nothing else. A marketplace serving sellers in another language supplies that locale itself, through `i18n.addResourceBundle` in the host app.
|
|
692
|
+
|
|
693
|
+
**Read more:** [Dashboard overview](../dashboard/overview.md) for the package split and both customization paths, [Dashboard customization](../dashboard/customization/quickstart.md) for the registries in detail, and [Plugins](../dashboard/plugins/overview.md) for shipping a redistributable one.
|
|
694
|
+
|
|
695
|
+
## Build against the Seller API
|
|
696
|
+
|
|
697
|
+
If you are not using the shipped panel — a native seller app, an ERP integration, a bespoke portal — the Seller API is the surface, and `@spree/seller-sdk` is the client.
|
|
698
|
+
|
|
699
|
+
**Every request is scoped to the seller making it. No endpoint takes a seller ID.** That is the security property worth relying on: a seller cannot ask for another seller's data, because there is nowhere in the request to name one.
|
|
700
|
+
|
|
701
|
+
```bash
|
|
702
|
+
npm install @spree/seller-sdk
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
```typescript
|
|
706
|
+
import { createSellerClient } from '@spree/seller-sdk'
|
|
707
|
+
|
|
708
|
+
const sellerClient = createSellerClient({ baseUrl: 'https://marketplace.example.com' })
|
|
709
|
+
|
|
710
|
+
const tokens = await sellerClient.auth.login({
|
|
711
|
+
email: 'owner@brightsparks.example',
|
|
712
|
+
password: '…',
|
|
713
|
+
})
|
|
714
|
+
sellerClient.setToken(tokens.access_token)
|
|
715
|
+
|
|
716
|
+
// A user who runs several sellers picks which one they are acting as
|
|
717
|
+
sellerClient.setSeller('sel_xxx')
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
Authentication is JWT only. **There is deliberately no secret-key equivalent**: a key that acts as a seller without a seller signing in is exactly what the Seller API's design exists to prevent. Sign-in fails for a store staff member who runs no seller, even though staff share the same user class.
|
|
721
|
+
|
|
722
|
+
The chosen seller travels as `X-Spree-Seller-Id`. The store is derived from the seller server-side and never sent alongside, so no header a client can set widens what it reaches.
|
|
723
|
+
|
|
724
|
+
### What the client covers
|
|
725
|
+
|
|
726
|
+
| Namespace | Notable methods |
|
|
727
|
+
|---|---|
|
|
728
|
+
| `auth` | `login`, `refresh`, `logout`, `providers`, `lookupInvitation`, `acceptInvitation`, `requestPasswordReset` |
|
|
729
|
+
| `onboarding` | `get`, `submitForReview`, `payoutAccount` |
|
|
730
|
+
| `requirementSubmissions` | `create` — create only; the seller submits again rather than editing |
|
|
731
|
+
| `profile`, `taxIdentifiers`, `policies` | The seller's own identity and legal documents |
|
|
732
|
+
| `team`, `invitations` | The seller hires and manages their own staff |
|
|
733
|
+
| `products` | Full CRUD plus `submit`, `draft`, `archive` and their bulk equivalents |
|
|
734
|
+
| `orders` | List and detail, plus `cancel`, `address`, nested `fulfillments`, `deliveries`, `labels`, `returns`, `exchanges`, `claims`, `notes` |
|
|
735
|
+
| `balances`, `transfers`, `payouts` | Read-only — written by fulfilment and the sweep, never by the seller |
|
|
736
|
+
| `deliveryMethods`, `packageTypes`, `stockLocations` | How and from where they ship |
|
|
737
|
+
| `imports`, `exports`, `directUploads` | Bulk listing by CSV, and CSV of what they sold |
|
|
738
|
+
| `productTypes`, `deliveryProfiles`, `deliveryZones`, `returnReasons`, `claimReasons`, `trackingCarriers`, `countries` | Read-only marketplace vocabulary a seller picks from |
|
|
739
|
+
|
|
740
|
+
Two asymmetries are worth noticing, because both are deliberate:
|
|
741
|
+
|
|
742
|
+
- **A seller cannot set a product's status.** They call `submit`, and the marketplace decides. Bulk moves exist only for the transitions a seller may make alone.
|
|
743
|
+
- **A seller cannot mark an order delivered.** That a parcel arrived is the buyer's word, not the sender's, so confirming receipt stays with the operator and the carrier feed.
|
|
744
|
+
|
|
745
|
+
What a seller's staff may do within all this is governed by [roles the seller owns](../core-concepts/staff-roles.md) — the same permission system as the back office, with a narrower set of keys, so a seller role can never reach store settings.
|
|
746
|
+
|
|
747
|
+
**Read more:** [Seller API introduction](../../api-reference/seller-api/introduction.md), [Seller API authentication](../../api-reference/seller-api/authentication.md), [Seller API errors](../../api-reference/seller-api/errors.md).
|
|
748
|
+
|
|
749
|
+
## Related
|
|
750
|
+
|
|
751
|
+
- [Sellers](../core-concepts/sellers.md) — the full model: lifecycle, ownership, order splitting, ledger
|
|
752
|
+
- [Commissions](../core-concepts/commissions.md) — rates, rules, precedence, currency handling, commission tax
|
|
753
|
+
- [Products](../core-concepts/products.md#seller-submissions) — the listing review flow in detail
|
|
754
|
+
- [Orders](../core-concepts/orders.md) — orders and their statuses
|
|
755
|
+
- [Customers](../core-concepts/customers.md) — who buys, across every seller
|
|
756
|
+
- [Delivery setup](../core-concepts/delivery-setup.md#on-a-marketplace) — how a seller's goods ship and what they ship in
|
|
757
|
+
- [Staff & Roles](../core-concepts/staff-roles.md) — how seller teams are governed
|
|
758
|
+
- [Seller payouts](../providers/payouts.md) — writing your own payout provider
|
|
759
|
+
- [Stripe Connect](../../integrations/payments/stripe-connect.md) — the shipped payout provider
|
|
760
|
+
- [Seller API](../../api-reference/seller-api/introduction.md) — the full endpoint reference
|
|
761
|
+
- [Dashboard overview](../dashboard/overview.md) — customizing either panel
|
|
762
|
+
- [Sellers (operator guide)](/user/sellers/sellers) — running a marketplace from the dashboard
|
|
763
|
+
- [Seller requirements (operator guide)](/user/settings/seller-requirements) — building the checklist
|
|
764
|
+
- [Payouts settings (operator guide)](/user/settings/marketplace) — who pays sellers and how often
|
|
@@ -10,7 +10,7 @@ At minimum, set `SPREE_API_URL` and `SPREE_PUBLISHABLE_KEY` in your hosting plat
|
|
|
10
10
|
## Production Build
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
|
|
13
|
+
pnpm build
|
|
14
14
|
npm start
|
|
15
15
|
```
|
|
16
16
|
|
|
@@ -102,8 +102,8 @@ For any platform that supports Node.js (Render, Railway, Fly.io, AWS, etc.):
|
|
|
102
102
|
1. Install dependencies and build:
|
|
103
103
|
|
|
104
104
|
```bash
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
pnpm install --frozen-lockfile
|
|
106
|
+
pnpm build
|
|
107
107
|
```
|
|
108
108
|
|
|
109
109
|
2. Start the production server:
|
|
@@ -130,13 +130,15 @@ jobs:
|
|
|
130
130
|
steps:
|
|
131
131
|
- uses: actions/checkout@v4
|
|
132
132
|
|
|
133
|
+
- uses: pnpm/action-setup@v4
|
|
134
|
+
|
|
133
135
|
- uses: actions/setup-node@v4
|
|
134
136
|
with:
|
|
135
137
|
node-version: 20
|
|
136
|
-
cache:
|
|
138
|
+
cache: pnpm
|
|
137
139
|
|
|
138
|
-
- run:
|
|
139
|
-
- run:
|
|
140
|
+
- run: pnpm install --frozen-lockfile
|
|
141
|
+
- run: pnpm build
|
|
140
142
|
|
|
141
143
|
# Add your deployment step here
|
|
142
144
|
# Example for Vercel:
|
|
@@ -28,7 +28,7 @@ Customize a template by editing its file directly — they use `@react-email/com
|
|
|
28
28
|
Run the storefront in development and open [http://localhost:3001/dev/emails](http://localhost:3001/dev/emails):
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
-
|
|
31
|
+
pnpm dev
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
Each template renders with sample data via `@react-email/render`. The route is gated to non-production environments.
|
|
@@ -51,7 +51,7 @@ To run the storefront on its own — against an existing Spree backend, or to wo
|
|
|
51
51
|
```bash
|
|
52
52
|
git clone https://github.com/spree/storefront.git
|
|
53
53
|
cd storefront
|
|
54
|
-
|
|
54
|
+
pnpm install
|
|
55
55
|
```
|
|
56
56
|
|
|
57
57
|
Then follow [Configuration](#configuration) to point it at your Spree API. If you plan to customize and track upstream changes, [fork first](customization.md#tracking-upstream-updates).
|
|
@@ -78,7 +78,7 @@ See [Deployment](deployment.md) for all optional environment variables (Sentry,
|
|
|
78
78
|
## Development
|
|
79
79
|
|
|
80
80
|
```bash
|
|
81
|
-
|
|
81
|
+
pnpm dev
|
|
82
82
|
```
|
|
83
83
|
|
|
84
84
|
Open [http://localhost:3001](http://localhost:3001) in your browser.
|
|
@@ -88,7 +88,7 @@ Open [http://localhost:3001](http://localhost:3001) in your browser.
|
|
|
88
88
|
## Production Build
|
|
89
89
|
|
|
90
90
|
```bash
|
|
91
|
-
|
|
91
|
+
pnpm build
|
|
92
92
|
npm start
|
|
93
93
|
```
|
|
94
94
|
|
|
@@ -9,12 +9,12 @@ The storefront ships two test layers: fast unit/integration tests with Vitest, a
|
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
11
|
npm test # one-shot
|
|
12
|
-
|
|
12
|
+
pnpm test:watch # watch mode
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
## End-to-end (Playwright)
|
|
16
16
|
|
|
17
|
-
The E2E suite drives a browser against a real Spree backend. `
|
|
17
|
+
The E2E suite drives a browser against a real Spree backend. `pnpm e2e:up` boots the backend — Postgres, Redis, and the official `ghcr.io/spree/spree:latest` image from `e2e-backend/docker-compose.yml` — then seeds it and mints an API key through the official [`@spree/cli`](../../cli/quickstart.md) (`spree seed`, `spree sample-data`, `spree api-key create`), installed as a dev dependency. No `create-spree-app` setup is required.
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
20
|
# 1. Export a Stripe test-mode key pair from your own Stripe sandbox.
|
|
@@ -26,16 +26,16 @@ export STRIPE_SECRET_KEY=sk_test_…
|
|
|
26
26
|
|
|
27
27
|
# 2. Boot Spree + Postgres + Redis, seed sample data, register a Stripe
|
|
28
28
|
# payment gateway, mint a publishable key, and write .env.e2e.
|
|
29
|
-
|
|
29
|
+
pnpm e2e:up
|
|
30
30
|
|
|
31
31
|
# 3. Run the suite. Playwright boots `next dev` against .env.e2e.
|
|
32
|
-
|
|
32
|
+
pnpm test:e2e
|
|
33
33
|
|
|
34
34
|
# Optional: interactive UI mode.
|
|
35
|
-
|
|
35
|
+
pnpm test:e2e:ui
|
|
36
36
|
|
|
37
37
|
# Tear everything down.
|
|
38
|
-
|
|
38
|
+
pnpm e2e:down
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
The checkout test pays with card `4242 4242 4242 4242` through Stripe's [test mode](https://docs.stripe.com/keys). PaymentIntents land in whichever Stripe test account owns the keys you exported.
|
|
@@ -55,8 +55,8 @@ The E2E stack reads a `SPREE_IMAGE` env var and falls back to the stock image wh
|
|
|
55
55
|
If you already publish a customized Spree image:
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
|
-
SPREE_IMAGE=ghcr.io/your-org/your-spree:latest
|
|
59
|
-
|
|
58
|
+
SPREE_IMAGE=ghcr.io/your-org/your-spree:latest pnpm e2e:up
|
|
59
|
+
pnpm test:e2e
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
### A `create-spree-app` project's backend
|
|
@@ -69,13 +69,13 @@ docker build -t project-spree:e2e ./backend
|
|
|
69
69
|
|
|
70
70
|
# From apps/storefront — run E2E against it
|
|
71
71
|
cd apps/storefront
|
|
72
|
-
SPREE_IMAGE=project-spree:e2e
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
SPREE_IMAGE=project-spree:e2e pnpm e2e:up
|
|
73
|
+
pnpm test:e2e
|
|
74
|
+
pnpm e2e:down
|
|
75
75
|
```
|
|
76
76
|
|
|
77
77
|
> **NOTE:** New `create-spree-app` projects wire this into the generated storefront CI workflow automatically — the workflow builds `server/Dockerfile` and runs the storefront E2E against that image rather than stock Spree.
|
|
78
78
|
|
|
79
79
|
### Interactive / manual
|
|
80
80
|
|
|
81
|
-
To click through the storefront against a running backend (no E2E harness), point it there directly: set `SPREE_API_URL` and `SPREE_PUBLISHABLE_KEY` in `.env.local` (see [Environment Variables](environment-variables.md)) and run `
|
|
81
|
+
To click through the storefront against a running backend (no E2E harness), point it there directly: set `SPREE_API_URL` and `SPREE_PUBLISHABLE_KEY` in `.env.local` (see [Environment Variables](environment-variables.md)) and run `pnpm dev`.
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Upgrading to Spree 6.0
|
|
3
3
|
description: Guide to upgrading a Spree 5.6 application to Spree 6.0
|
|
4
|
-
hidden: true
|
|
5
4
|
---
|
|
6
5
|
|
|
7
6
|
> **INFO:** Before proceeding to upgrade, please ensure you're at [Spree 5.6](/v5/developer/upgrades/5.5-to-5.6). Spree 6.0 requires **Rails 8.1** and is the designated breaking-change window for the platform — read the behavioral changes section even if your upgrade runs clean.
|