@spree/docs 0.1.176 → 0.1.178
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/api-reference/store.yaml +48 -48
- package/dist/developer/cli/quickstart.md +1 -1
- package/dist/developer/contributing/creating-an-extension.md +2 -2
- package/dist/developer/core-concepts/addresses.md +3 -3
- package/dist/developer/core-concepts/architecture.md +4 -4
- package/dist/developer/core-concepts/calculators.md +2 -2
- package/dist/developer/core-concepts/carts.md +207 -0
- package/dist/developer/core-concepts/channels.md +3 -3
- package/dist/developer/core-concepts/customers.md +1 -1
- package/dist/developer/core-concepts/events.md +1 -1
- package/dist/developer/core-concepts/fulfillments.md +278 -0
- package/dist/developer/core-concepts/imports-exports.md +6 -18
- package/dist/developer/core-concepts/inventory.md +3 -3
- package/dist/developer/core-concepts/media.md +1 -1
- package/dist/developer/core-concepts/metafields.md +1 -1
- package/dist/developer/core-concepts/orders.md +92 -339
- package/dist/developer/core-concepts/payments.md +1 -1
- package/dist/developer/core-concepts/promotions.md +68 -195
- package/dist/developer/core-concepts/returns-exchanges-claims.md +173 -0
- package/dist/developer/core-concepts/store-credits-gift-cards.md +16 -18
- package/dist/developer/core-concepts/stores.md +1 -1
- package/dist/developer/core-concepts/taxes-discounts-fees.md +199 -0
- package/dist/developer/core-concepts/taxes.md +3 -3
- package/dist/developer/create-spree-app/quickstart.md +1 -1
- package/dist/developer/customization/api.md +36 -11
- package/dist/developer/customization/configuration.md +207 -37
- package/dist/developer/customization/decorators.md +8 -8
- package/dist/developer/customization/permissions.md +34 -229
- package/dist/developer/customization/quickstart.md +159 -108
- package/dist/developer/customization/validations.md +1 -1
- package/dist/developer/dashboard/overview.md +1 -1
- package/dist/developer/how-to/build-a-b2b-store.md +21 -0
- package/dist/developer/how-to/build-a-marketplace.md +25 -0
- package/dist/developer/how-to/custom-api-authentication.md +1 -1
- package/dist/developer/how-to/custom-delivery-rate-provider.md +242 -0
- package/dist/developer/how-to/custom-document-numbers.md +154 -0
- package/dist/developer/how-to/custom-order-routing.md +1 -1
- package/dist/developer/how-to/custom-payment-method.md +1 -1
- package/dist/developer/how-to/custom-promotion.md +76 -115
- package/dist/developer/how-to/custom-report.md +2 -2
- package/dist/developer/how-to/custom-stock-splitter.md +5 -5
- package/dist/developer/how-to/sell-digital-products.md +20 -0
- package/dist/developer/multi-tenant/quickstart.md +1 -1
- package/dist/developer/providers/dam.md +14 -0
- package/dist/developer/providers/erp.md +31 -0
- package/dist/developer/providers/fulfillment.md +25 -0
- package/dist/developer/{deployment/telemetry.md → providers/observability.md} +1 -1
- package/dist/developer/providers/overview.md +44 -0
- package/dist/developer/providers/pim.md +25 -0
- package/dist/developer/providers/sso.md +20 -0
- package/dist/developer/sdk/admin/extending.md +1 -1
- package/dist/developer/sdk/admin/quickstart.md +1 -1
- package/dist/developer/sdk/admin/resources.md +1 -1
- package/dist/developer/sdk/extending.md +1 -1
- package/dist/developer/tutorial/admin-api.md +13 -0
- package/dist/developer/tutorial/dashboard-plugin.md +14 -0
- package/dist/developer/tutorial/events.md +7 -161
- package/dist/developer/tutorial/introduction.md +15 -35
- package/dist/developer/tutorial/model.md +7 -98
- package/dist/developer/tutorial/store-api.md +13 -0
- package/dist/developer/tutorial/storefront.md +12 -0
- package/dist/developer/tutorial/testing.md +7 -711
- package/dist/developer/upgrades/5.6-to-6.0.md +2 -2
- package/package.json +1 -1
- package/dist/developer/admin/admin.md +0 -214
- package/dist/developer/admin/authentication.md +0 -59
- package/dist/developer/admin/components.md +0 -711
- package/dist/developer/admin/custom-css.md +0 -256
- package/dist/developer/admin/custom-javascript.md +0 -116
- package/dist/developer/admin/extending-ui.md +0 -1839
- package/dist/developer/admin/form-builder.md +0 -444
- package/dist/developer/admin/helper-methods.md +0 -531
- package/dist/developer/admin/navigation.md +0 -805
- package/dist/developer/admin/tables.md +0 -490
- package/dist/developer/advanced/adding_spree_to_rails_app.md +0 -92
- package/dist/developer/core-concepts/adjustments.md +0 -113
- package/dist/developer/core-concepts/reports.md +0 -208
- package/dist/developer/core-concepts/shipments.md +0 -307
- package/dist/developer/core-concepts/users.md +0 -303
- package/dist/developer/customization/authentication.md +0 -100
- package/dist/developer/customization/checkout.md +0 -202
- package/dist/developer/customization/emails.md +0 -18
- package/dist/developer/customization/routes.md +0 -24
- package/dist/developer/multi-vendor/installation.md +0 -61
- package/dist/developer/multi-vendor/quickstart.md +0 -17
- package/dist/developer/tutorial/admin.md +0 -206
- package/dist/developer/tutorial/api.md +0 -606
- package/dist/developer/tutorial/extending-models.md +0 -393
- package/dist/developer/tutorial/sdk.md +0 -170
- package/dist/developer/upgrades/2.0-to-2.1.md +0 -46
- package/dist/developer/upgrades/2.1-to-2.2.md +0 -59
- package/dist/developer/upgrades/2.2-to-2.3.md +0 -44
- package/dist/developer/upgrades/2.3-to-2.4.md +0 -42
- package/dist/developer/upgrades/3.0-to-3.1.md +0 -47
- package/dist/developer/upgrades/3.1-to-3.2.md +0 -34
- package/dist/developer/upgrades/3.2-to-3.3.md +0 -70
- package/dist/developer/upgrades/3.3-to-3.4.md +0 -36
- package/dist/developer/upgrades/3.4-to-3.5.md +0 -44
- package/dist/developer/upgrades/3.5-to-3.6.md +0 -40
- package/dist/developer/upgrades/3.6-to-3.7.md +0 -62
- package/dist/developer/upgrades/3.7-to-4.0.md +0 -152
- package/dist/developer/upgrades/4.0-to-4.1.md +0 -92
- package/dist/developer/upgrades/4.1-to-4.2.md +0 -109
- package/dist/developer/upgrades/4.10-to-5.0.md +0 -131
- package/dist/developer/upgrades/4.2-to-4.3.md +0 -100
- package/dist/developer/upgrades/4.3-to-4.4.md +0 -125
- package/dist/developer/upgrades/4.4-to-4.5.md +0 -94
- package/dist/developer/upgrades/4.5-to-4.6.md +0 -119
- package/dist/developer/upgrades/4.6-to-4.7.md +0 -39
- package/dist/developer/upgrades/4.8-to-4.9.md +0 -24
- package/dist/developer/upgrades/4.9-to-4.10.md +0 -24
- package/dist/developer/upgrades/4.x-to-4.8.md +0 -52
- package/dist/developer/upgrades/5.0-to-5.1.md +0 -28
- package/dist/developer/upgrades/5.1-to-5.2.md +0 -131
- package/dist/developer/upgrades/5.2-to-5.3.md +0 -338
- package/dist/developer/upgrades/5.3-to-5.4.md +0 -277
- package/dist/developer/upgrades/5.4-to-5.5.md +0 -301
- package/dist/developer/upgrades/5.5-to-5.6.md +0 -207
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Taxes, Discounts & Fees
|
|
3
|
+
description: How Spree records money added to or taken off an order — separate tax lines, discounts and fees, and the order totals you render in a storefront.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
An order total is rarely just the sum of item prices. Tax gets added, a promo code takes something off, gift wrapping costs extra. Spree records each of those as its own row, of one of three kinds:
|
|
9
|
+
|
|
10
|
+
| Kind | Effect | What it is |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| **Tax line** | adds | Tax charged on an item, on delivery, or on a fee |
|
|
13
|
+
| **Discount** | subtracts | A promotion or a manual reduction |
|
|
14
|
+
| **Fee** | adds | A surcharge — handling, gift wrap, cash on delivery |
|
|
15
|
+
|
|
16
|
+
Keeping them apart means a direct question gets a direct answer. "What tax did we charge on this order?" is one request against tax lines, rather than filtering a mixed list and hoping you caught every case.
|
|
17
|
+
|
|
18
|
+
```mermaid
|
|
19
|
+
erDiagram
|
|
20
|
+
Order ||--o{ TaxLine : "has many"
|
|
21
|
+
Order ||--o{ Discount : "has many"
|
|
22
|
+
Order ||--o{ Fee : "has many"
|
|
23
|
+
LineItem ||--o{ TaxLine : "taxed by"
|
|
24
|
+
LineItem ||--o{ Discount : "reduced by"
|
|
25
|
+
Fulfillment ||--o{ TaxLine : "taxed by"
|
|
26
|
+
Fee ||--o{ TaxLine : "taxed by"
|
|
27
|
+
|
|
28
|
+
TaxLine {
|
|
29
|
+
string label
|
|
30
|
+
string rate
|
|
31
|
+
boolean included
|
|
32
|
+
string amount
|
|
33
|
+
}
|
|
34
|
+
Discount {
|
|
35
|
+
string label
|
|
36
|
+
string kind
|
|
37
|
+
string code
|
|
38
|
+
string amount
|
|
39
|
+
}
|
|
40
|
+
Fee {
|
|
41
|
+
string label
|
|
42
|
+
string kind
|
|
43
|
+
string amount
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
> **NOTE:** Money is sent as a **string** — `"15.99"`, not `15.99`. JavaScript numbers can't represent every decimal exactly, and `0.1 + 0.2` gives `0.30000000000000004`, which is not something you want inside a total. If you need to do arithmetic, use a decimal library or work in whole cents.
|
|
48
|
+
|
|
49
|
+
## Building an order summary
|
|
50
|
+
|
|
51
|
+
Most storefronts never need the individual rows. The order already carries a total for each kind, so you can render a summary directly:
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
const order = await client.orders.get('or_xxx')
|
|
55
|
+
|
|
56
|
+
order.display_item_total // "$120.00" items before tax and discounts
|
|
57
|
+
order.display_discount_total // "-$12.00" everything taken off
|
|
58
|
+
order.display_delivery_total // "$5.00" delivery
|
|
59
|
+
order.display_tax_total // "$22.60" all tax
|
|
60
|
+
order.display_total // "$135.60" what the customer pays
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Every total comes in two forms: `total` is the raw value (`"135.60"`) and `display_total` is formatted for the order's currency (`"$135.60"`). Render the `display_` one — it handles the symbol, decimal separator and where the symbol goes.
|
|
64
|
+
|
|
65
|
+
| Attribute | What it sums |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `item_total` | Line item prices before tax and discounts |
|
|
68
|
+
| `discount_total` | All discounts |
|
|
69
|
+
| `tax_total` | All tax |
|
|
70
|
+
| `included_tax_total` | Tax already inside displayed prices |
|
|
71
|
+
| `additional_tax_total` | Tax added on top |
|
|
72
|
+
| `delivery_total` | Delivery charges |
|
|
73
|
+
| `total` | What the customer pays |
|
|
74
|
+
|
|
75
|
+
> **WARNING:** If you add up a subtotal yourself, include `additional_tax_total` only. `included_tax_total` is **already inside** the item prices — adding it again counts the tax twice. This is the most common mistake when building a European storefront.
|
|
76
|
+
|
|
77
|
+
## Tax lines
|
|
78
|
+
|
|
79
|
+
A tax line records one tax charge and what it was charged on.
|
|
80
|
+
|
|
81
|
+
| Attribute | Description |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `label` | What the customer sees, e.g. `VAT 20%` |
|
|
84
|
+
| `rate` | The rate applied, e.g. `"0.2"` |
|
|
85
|
+
| `included` | Whether the tax was already inside the displayed price |
|
|
86
|
+
| `amount` / `display_amount` | Tax charged |
|
|
87
|
+
| `line_item_id` / `fulfillment_id` / `fee_id` | What was taxed — exactly one is set |
|
|
88
|
+
| `tax_rate_id` | The configured rate, when tax came from Spree rather than an outside service |
|
|
89
|
+
|
|
90
|
+
`included` is the field that trips people up. In most of Europe a €120 price already contains €20 of VAT, so the tax line is there for information — the customer pays €120, not €140. In the US, tax is added at checkout, so the same field is `false` and the amount really is extra.
|
|
91
|
+
|
|
92
|
+
```typescript Admin SDK
|
|
93
|
+
const taxLines = await adminClient.orders.taxLines.list('or_xxx')
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
You never create tax lines yourself. They're written whenever the order total is worked out — see [Taxes](taxes.md).
|
|
97
|
+
|
|
98
|
+
## Discounts
|
|
99
|
+
|
|
100
|
+
| Attribute | Description |
|
|
101
|
+
|---|---|
|
|
102
|
+
| `label` | What the customer sees, e.g. `Summer Sale` |
|
|
103
|
+
| `kind` | `promotion` or `manual` |
|
|
104
|
+
| `code` | The coupon code used, if any |
|
|
105
|
+
| `value` / `value_type` | The rule behind it, e.g. `"10"` / `percent`. Accepts a string or a number on write. |
|
|
106
|
+
| `amount` / `display_amount` | The reduction, as a negative amount |
|
|
107
|
+
| `promotion_id` | The promotion it came from |
|
|
108
|
+
| `line_item_id` / `fulfillment_id` | What it reduced |
|
|
109
|
+
|
|
110
|
+
Most discounts come from [Promotions](promotions.md) — a customer applies a code and the rows appear. A **manual** discount is one an admin adds directly, for a goodwill gesture or a price match:
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
```typescript Admin SDK
|
|
114
|
+
// 10% off a single line item
|
|
115
|
+
await adminClient.orders.discounts.create('or_xxx', {
|
|
116
|
+
label: 'Goodwill discount',
|
|
117
|
+
value: '10',
|
|
118
|
+
value_type: 'percent',
|
|
119
|
+
line_item_id: 'li_xxx',
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// A flat amount across the order
|
|
123
|
+
await adminClient.orders.discounts.create('or_xxx', {
|
|
124
|
+
label: 'Price match',
|
|
125
|
+
value: '15.00',
|
|
126
|
+
value_type: 'flat',
|
|
127
|
+
})
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```bash cURL
|
|
131
|
+
curl -X POST 'https://api.mystore.com/api/v3/admin/orders/or_xxx/discounts' \
|
|
132
|
+
-H 'X-Spree-API-Key: sk_xxx' \
|
|
133
|
+
-H 'Content-Type: application/json' \
|
|
134
|
+
-d '{ "label": "Goodwill discount", "value": 10, "value_type": "percent" }'
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
Only manual discounts can be edited or deleted. Changing one that came from a promotion returns a `422` — those belong to the promotion that created them, and letting an order disagree with its own promotion is how reporting stops adding up.
|
|
139
|
+
|
|
140
|
+
A discount on the whole order is shared out across the items rather than kept as one lump sum. So if the customer later returns one item, Spree knows how much of the discount belonged to it.
|
|
141
|
+
|
|
142
|
+
## Fees
|
|
143
|
+
|
|
144
|
+
| Attribute | Description |
|
|
145
|
+
|---|---|
|
|
146
|
+
| `label` | What the customer sees, e.g. `Gift wrapping` |
|
|
147
|
+
| `kind` | `surcharge`, `handling`, `gift_wrap`, `cod`, `payment`, `duty`, or your own |
|
|
148
|
+
| `amount` / `display_amount` | The charge |
|
|
149
|
+
| `line_item_id` / `fulfillment_id` | What it applies to — both empty means the whole order |
|
|
150
|
+
|
|
151
|
+
```typescript Admin SDK
|
|
152
|
+
await adminClient.orders.fees.create('or_xxx', {
|
|
153
|
+
label: 'Gift wrapping',
|
|
154
|
+
kind: 'gift_wrap',
|
|
155
|
+
amount: '5.00',
|
|
156
|
+
line_item_id: 'li_xxx',
|
|
157
|
+
})
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Fees are always positive, and they're taxable by default — a fee can have its own tax lines. Customs duties are the one exception, described below. To take money off, create a discount instead. That way a refund never has to work out whether a negative fee was a charge or a credit.
|
|
161
|
+
|
|
162
|
+
### Customs duties
|
|
163
|
+
|
|
164
|
+
A `duty` fee is the one kind tax is **not** applied to automatically. A customs duty is an import charge levied by the destination country, not a sale that domestic tax applies to, so charging tax on top of it would invent tax the merchant never owed. Where import VAT is charged on a duty, whoever calculates the duty records that tax itself.
|
|
165
|
+
|
|
166
|
+
A duty also keeps a copy of what it was calculated from — the HS code, the country of origin, the rate — in its `metadata`. Classification on a product can change next month; what the customer was charged must not.
|
|
167
|
+
|
|
168
|
+
## Where rows attach
|
|
169
|
+
|
|
170
|
+
Each row points at exactly the thing it applies to:
|
|
171
|
+
|
|
172
|
+
- **Tax lines** → a line item, a [fulfillment](fulfillments.md), or a fee, so item tax and delivery tax stay separately visible
|
|
173
|
+
- **Discounts** → a line item or a fulfillment
|
|
174
|
+
- **Fees** → a line item, a fulfillment, or the order itself
|
|
175
|
+
|
|
176
|
+
Rows also keep a copy of where they came from — the rate and label on a tax line, the code and promotion on a discount. If someone deletes that promotion next month, the order still says what the customer was actually given.
|
|
177
|
+
|
|
178
|
+
## When totals change
|
|
179
|
+
|
|
180
|
+
Totals are worked out again whenever something changes on a cart — an item added, an address entered, a code applied. The response to every cart change already includes the new totals, so a follow-up request is never needed:
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
// The returned cart already has updated totals
|
|
184
|
+
const cart = await client.carts.items.create(cartId, {
|
|
185
|
+
variant_id: 'var_xxx',
|
|
186
|
+
quantity: 2,
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
cart.display_total // already correct
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Once an order is placed, the rows stop being regenerated. Editing a placed order re-adds its existing rows rather than starting from scratch, so today's promotions can't quietly rewrite what a customer agreed to last week.
|
|
193
|
+
|
|
194
|
+
## Related
|
|
195
|
+
|
|
196
|
+
- [Taxes](taxes.md) — how tax is worked out
|
|
197
|
+
- [Promotions](promotions.md) — where most discounts come from
|
|
198
|
+
- [Fulfillments](fulfillments.md) — delivery charges
|
|
199
|
+
- [Orders](orders.md) — order totals and statuses
|
|
@@ -37,7 +37,7 @@ erDiagram
|
|
|
37
37
|
- **Tax Category** groups products for tax purposes (e.g., "Clothing", "Food", "Digital")
|
|
38
38
|
- **Tax Rate** defines the percentage and rules for a Tax Category within a [Zone](addresses.md#zones)
|
|
39
39
|
- **Zone** defines geographic regions (countries or states) where taxes apply
|
|
40
|
-
- **[Adjustments](
|
|
40
|
+
- **[Adjustments](taxes-discounts-fees.md)** are created on orders/line items to apply taxes
|
|
41
41
|
|
|
42
42
|
## Tax Categories
|
|
43
43
|
|
|
@@ -127,7 +127,7 @@ During checkout, taxes are calculated based on the shipping address:
|
|
|
127
127
|
|
|
128
128
|
1. The customer's shipping address determines their [Zone](addresses.md#zones)
|
|
129
129
|
2. Spree finds matching tax rates for that zone and the product's tax category
|
|
130
|
-
3. Tax [Adjustments](
|
|
130
|
+
3. Tax [Adjustments](taxes-discounts-fees.md) are created on line items
|
|
131
131
|
4. The order total is updated
|
|
132
132
|
|
|
133
133
|
> **NOTE:** For complex tax requirements (interstate US sales, international VAT), consider using automated tax calculation. The [Stripe Tax integration](../../integrations/payments/stripe.md) handles this automatically.
|
|
@@ -158,6 +158,6 @@ Tax categories and rates are managed in the Admin Panel under **Settings → Tax
|
|
|
158
158
|
|
|
159
159
|
- [Markets](markets.md) — Tax-inclusive pricing per market
|
|
160
160
|
- [Addresses](addresses.md) — Zones and address-based taxation
|
|
161
|
-
- [Adjustments](
|
|
161
|
+
- [Adjustments](taxes-discounts-fees.md) — Tax adjustments
|
|
162
162
|
- [Orders](orders.md) — How taxes affect order totals
|
|
163
163
|
- [Avalara integration](../../integrations/tax/avalara.md) — Automated tax calculation for complex jurisdictions
|
|
@@ -5,7 +5,7 @@ description: Add new Store API endpoints, customize existing JSON responses with
|
|
|
5
5
|
|
|
6
6
|
Before you start customizing Spree API endpoints, make sure you reviewed all existing API endpoints in the [Spree API docs](../../api-reference/admin-api/endpoints.md).
|
|
7
7
|
|
|
8
|
-
For a step-by-step walkthrough of adding a complete new resource (model, serializer, controller, routes), see the [API tutorial](../tutorial/api.md).
|
|
8
|
+
For a step-by-step walkthrough of adding a complete new resource (model, serializer, controller, routes), see the [API tutorial](../tutorial/admin-api.md).
|
|
9
9
|
|
|
10
10
|
## Customizing JSON Responses
|
|
11
11
|
|
|
@@ -35,7 +35,7 @@ Restart the server and the Product API will include your new attribute.
|
|
|
35
35
|
|
|
36
36
|
### Adding an Association
|
|
37
37
|
|
|
38
|
-
Let's say you've created a `Spree::Brand` model that belongs to `Product` (see the [tutorial](../tutorial/api.md) for the full example).
|
|
38
|
+
Let's say you've created a `Spree::Brand` model that belongs to `Product` (see the [tutorial](../tutorial/admin-api.md) for the full example).
|
|
39
39
|
|
|
40
40
|
Create a serializer for the new model:
|
|
41
41
|
|
|
@@ -173,20 +173,45 @@ end
|
|
|
173
173
|
|
|
174
174
|
### Permitted Attributes
|
|
175
175
|
|
|
176
|
-
For endpoints that accept writes (create/update),
|
|
176
|
+
For endpoints that accept writes (create/update), declare the attributes in your
|
|
177
|
+
controller:
|
|
177
178
|
|
|
178
|
-
```ruby
|
|
179
|
-
|
|
179
|
+
```ruby
|
|
180
|
+
def resource_permitted_attributes
|
|
181
|
+
[:name, :slug, :description, :logo]
|
|
182
|
+
end
|
|
180
183
|
```
|
|
181
184
|
|
|
182
|
-
|
|
185
|
+
Entries are `params.permit` fragments, so collections and nested structures use
|
|
186
|
+
the shapes you would pass there directly — `[:name, { region_ids: [] }]`.
|
|
183
187
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
+
> **NOTE:** Declare `resource_permitted_attributes`, not `permitted_attributes`. The
|
|
189
|
+
> latter is what appends extension-contributed attributes to your list, so
|
|
190
|
+
> overriding it replaces them.
|
|
191
|
+
|
|
192
|
+
### Adding an Attribute to a Core Resource
|
|
193
|
+
|
|
194
|
+
To make a new column writable on a Spree model without decorating the
|
|
195
|
+
controllers that write it, append to the model's permitted attributes:
|
|
196
|
+
|
|
197
|
+
```ruby config/initializers/spree.rb
|
|
198
|
+
Spree::Product.additional_permitted_attributes += [:brand_id]
|
|
188
199
|
```
|
|
189
200
|
|
|
201
|
+
Standard resource endpoints append these to their own allowlist, so one
|
|
202
|
+
declaration covers the model's create and update endpoints.
|
|
203
|
+
|
|
204
|
+
Use `+=`, never `=` — assigning replaces what another extension added — and
|
|
205
|
+
declare only attributes of your own, since redeclaring a key the controller
|
|
206
|
+
already permits replaces its filter rather than widening it.
|
|
207
|
+
|
|
208
|
+
> **NOTE:** For merchant-managed data, prefer [Custom Fields](../core-concepts/metafields.md),
|
|
209
|
+
> which need no code and are filterable and sortable. This is for extensions
|
|
210
|
+
> that add real database columns.
|
|
211
|
+
|
|
212
|
+
Two endpoints deliberately ignore this hook because their parameters are
|
|
213
|
+
authorization data rather than resource data: API keys and invitations.
|
|
214
|
+
|
|
190
215
|
## Consuming Custom Endpoints with the SDK
|
|
191
216
|
|
|
192
|
-
See the [SDK tutorial](../tutorial/
|
|
217
|
+
See the [SDK tutorial](../tutorial/storefront.md) for how to call custom endpoints from TypeScript and extend the generated types.
|
|
@@ -1,55 +1,225 @@
|
|
|
1
1
|
---
|
|
2
|
-
title:
|
|
2
|
+
title: Configuration
|
|
3
|
+
description: Every global Spree setting that is actually read by Spree 6 — grouped by area, with defaults and what each one changes.
|
|
3
4
|
---
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
| Configuration Key | Description | Default Value |
|
|
8
|
-
|------------------------------------|-------------------------------------------------------------------------------------------------------|---------------------|
|
|
9
|
-
| `allow_checkout_on_gateway_error` | Continues the checkout process even if the payment gateway error failed. | `false` |
|
|
10
|
-
| `address_requires_phone` | Determines whether a phone number is required for Addresses. | `false` |
|
|
11
|
-
| `alternative_shipping_phone` | Determines if an alternative phone number should be present for the shipping address on the checkout page. | `false` |
|
|
12
|
-
| `always_include_confirm_step` | Determines if the confirmation step is always included in the checkout process, regardless of the payment method. | `false` |
|
|
13
|
-
| `auto_capture` | Depending on whether or not Spree is configured to "auto capture" the credit card, either a purchase or an authorize operation will be performed on the card (via the current credit card gateway). | `true` |
|
|
14
|
-
| `auto_capture_on_dispatch` | Captures payment for each shipment in Shipment#after_ship callback, and makes Shipment.ready when payment authorized. | `false` |
|
|
15
|
-
| `company` | Determines whether or not a field for "Company" displays on the address form. | `false` |
|
|
16
|
-
| `credit_to_new_allocation` | Determines if a new allocation is created anytime store credit is added. If not set, it will update the store credit's amount in place. | `false` |
|
|
17
|
-
| `disable_sku_validation` | Determines if the built-in SKU uniqueness validation is disabled. | `false` |
|
|
18
|
-
| `disable_store_presence_validation` | Determines if Store presence validation for Products and Payment Methods is disabled. | `false` |
|
|
19
|
-
| `expedited_exchanges` | Determines if an exchange shipment is kicked off upon return authorization save. Requires payment profiles to be supported on your gateway and a configured delayed job handler. | `false` |
|
|
20
|
-
| `expedited_exchanges_days_window` | The number of days the customer has to return their item after the expedited exchange is shipped to avoid being charged. | `14` |
|
|
21
|
-
| `restock_inventory` | Determines if inventory should be restocked when an order is canceled or returned | `true` |
|
|
22
|
-
| `return_eligibility_number_of_days` | The number of days after purchase within which a return can be initiated. | `365` |
|
|
23
|
-
| `show_products_without_price` | Determines if products without a price are shown in the storefront and Storefront API | `false` |
|
|
24
|
-
| `tax_using_ship_address` | Determines if tax information should be based on shipping address, rather than the billing address. | `true` |
|
|
25
|
-
| `track_inventory_levels` | Determines if inventory levels should be tracked when products are purchased at checkout. This option causes new `InventoryUnit` objects to be created when a product is bought. | `true` |
|
|
26
|
-
|
|
27
|
-
## Spree Initializer
|
|
28
|
-
|
|
29
|
-
To change values for these preferences, you need to edit your `config/initializers/spree.rb` file.
|
|
30
|
-
|
|
31
|
-
For example, to disable the `expedited_exchanges` feature, you would add the following line:
|
|
6
|
+
Global settings live in `config/initializers/spree.rb` and apply to the whole application. Anything that varies per store — currency, markets, delivery zones, languages — is data you change in the dashboard instead.
|
|
32
7
|
|
|
33
|
-
```ruby
|
|
8
|
+
```ruby config/initializers/spree.rb
|
|
34
9
|
Spree.config do |config|
|
|
35
|
-
config.
|
|
10
|
+
config.minimum_password_length = 10
|
|
11
|
+
config.allow_empty_price_amount = true
|
|
36
12
|
end
|
|
37
13
|
```
|
|
38
14
|
|
|
39
|
-
> **NOTE:**
|
|
15
|
+
> **NOTE:** Restart your Rails server after editing the initializer.
|
|
16
|
+
|
|
17
|
+
## Deployment settings read the environment
|
|
18
|
+
|
|
19
|
+
Settings that describe the deployment rather than commerce behavior can be set with an environment variable — no Ruby, no code change, no restart-after-edit dance. An explicitly configured value always wins over the environment.
|
|
20
|
+
|
|
21
|
+
| Setting | Environment variable | What it configures |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| `dashboard_url` | `SPREE_DASHBOARD_URL` | Origin where the admin dashboard is hosted (e.g. `https://admin.my-shop.com`). Used for every link that sends someone into the dashboard: invitation emails, SSO redirects, and the first-run setup link printed by `db:seed`. |
|
|
24
|
+
|
|
25
|
+
This page lists only settings Spree 6 actually reads. Settings that exist for backwards compatibility are listed under [Deprecated settings](#deprecated-settings) rather than mixed in with the rest.
|
|
26
|
+
|
|
27
|
+
## Settings that belong to a store
|
|
28
|
+
|
|
29
|
+
Settings that shape how a shop sells — when customers are charged, whether stock is counted, which products appear without a price — are properties of a store, not of the installation, so they live on `Spree::Store` and are edited in the dashboard under **Settings → Store**:
|
|
40
30
|
|
|
41
|
-
|
|
31
|
+
`capture_method`, `track_inventory_levels`, `stock_reservations_enabled`, `stock_reservation_ttl_minutes`, `track_price_history`, `show_products_without_price`, `disable_sku_validation`, `address_requires_phone`, `company_field_enabled`, `tax_using_ship_address`, plus `guest_checkout` and `storefront_access` — those last two are owned by the channel, and the store's value is what a channel falls back to.
|
|
42
32
|
|
|
43
|
-
|
|
33
|
+
They were global before Spree 6. The global versions still exist for one release but are no longer read, so setting one in the initializer changes nothing — run `bin/rake spree:store_settings:backfill_from_config` during the upgrade to copy your values onto every store.
|
|
44
34
|
|
|
45
35
|
```ruby
|
|
46
|
-
Spree::
|
|
36
|
+
store = Spree::Current.store
|
|
37
|
+
store.preferred_capture_method # => "checkout"
|
|
38
|
+
store.update!(preferred_track_inventory_levels: false)
|
|
47
39
|
```
|
|
48
40
|
|
|
49
|
-
|
|
41
|
+
### When customers are charged
|
|
42
|
+
|
|
43
|
+
`capture_method` says when a customer is charged rather than only authorized. It applies to every payment method, not just card gateways. It takes one of three values:
|
|
44
|
+
|
|
45
|
+
| Value | What happens |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `checkout` | The money is taken as soon as the order is placed. This is the default. |
|
|
48
|
+
| `on_dispatch` | The amount is authorized at checkout and captured when the goods are dispatched. |
|
|
49
|
+
| `manual` | The amount is authorized at checkout and staff capture it when they choose. |
|
|
50
|
+
|
|
51
|
+
A payment method can override the store for its own payments by setting the same three values; leaving it blank inherits the store's choice. Ask a payment method what applies with `resolved_capture_method`, or use the `capture_at_checkout?`, `capture_on_dispatch?` and `capture_manually?` questions.
|
|
50
52
|
|
|
51
53
|
```ruby
|
|
52
|
-
|
|
54
|
+
payment_method.update!(capture_method: 'on_dispatch')
|
|
55
|
+
payment_method.resolved_capture_method # => "on_dispatch"
|
|
56
|
+
|
|
57
|
+
payment_method.update!(capture_method: nil) # back to inheriting
|
|
58
|
+
payment_method.resolved_capture_method # => "checkout"
|
|
53
59
|
```
|
|
54
60
|
|
|
55
|
-
|
|
61
|
+
Before Spree 6 this was two separate booleans, `auto_capture` and `auto_capture_on_dispatch`, which could contradict each other. Two upgrade tasks carry existing installs over: `spree:store_settings:backfill_from_config` maps the old globals onto each store's `capture_method`, and `spree:migrate_capture_methods` converts the per-payment-method `auto_capture` column. A method left without a value inherits its store.
|
|
62
|
+
|
|
63
|
+
## Checkout and payments
|
|
64
|
+
|
|
65
|
+
| Setting | Default | What it does |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| `always_include_confirm_step` | `false` | Always shows a confirmation step, even when the payment method doesn't need one. |
|
|
68
|
+
| `credit_to_new_allocation` | `false` | Records refunded store credit as a new entry each time instead of topping the original credit back up. |
|
|
69
|
+
|
|
70
|
+
## Carts and stock reservations
|
|
71
|
+
|
|
72
|
+
| Setting | Default | What it does |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `guest_cart_expiry_days` | `30` | How long an abandoned guest cart is kept before deletion. |
|
|
75
|
+
| `customer_cart_expiry_days` | `90` | How long a signed-in customer's abandoned cart is kept. |
|
|
76
|
+
| `empty_cart_expiry_hours` | `48` | How long a cart with no items is kept. |
|
|
77
|
+
|
|
78
|
+
## Products and pricing
|
|
79
|
+
|
|
80
|
+
| Setting | Default | What it does |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| `allow_empty_price_amount` | `false` | Allows a price to be left blank instead of requiring an amount. Prices in a price list already allow blank. |
|
|
83
|
+
| `price_history_retention_days` | `30` | How many days of price history the `spree:price_history:prune` task keeps. |
|
|
84
|
+
| `always_use_translations` | `false` | Always read product, category and collection names from the translation tables, even in the store's own default language. |
|
|
85
|
+
|
|
86
|
+
## Addresses
|
|
87
|
+
|
|
88
|
+
| Setting | Default | What it does |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `geocode_addresses` | `true` | Looks up latitude and longitude for saved addresses. |
|
|
91
|
+
|
|
92
|
+
## Media
|
|
93
|
+
|
|
94
|
+
| Setting | Default | What it does |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `max_image_download_size` | `20971520` | Largest file size, in bytes, accepted when downloading an image from an external URL. |
|
|
97
|
+
| `images_save_from_url_job_attempts` | `5` | How many times to retry fetching an external image before giving up. |
|
|
98
|
+
|
|
99
|
+
## Promotions and gift cards
|
|
100
|
+
|
|
101
|
+
| Setting | Default | What it does |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| `coupon_codes_web_limit` | `500` | How many coupon codes are created immediately when saving a promotion. Larger batches move to a background job so saving stays fast. |
|
|
104
|
+
| `coupon_codes_total_limit` | `5000` | Maximum coupon codes a single promotion may generate. |
|
|
105
|
+
| `gift_card_batch_web_limit` | `500` | How many gift card codes a batch creates immediately before switching to a background job. |
|
|
106
|
+
| `gift_card_batch_limit` | `50000` | Maximum gift card codes in a single batch. |
|
|
107
|
+
|
|
108
|
+
## Accounts and passwords
|
|
109
|
+
|
|
110
|
+
| Setting | Default | What it does |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| `minimum_password_length` | `8` | Shortest password a customer or staff member may set. |
|
|
113
|
+
| `maximum_password_length` | `72` | Longest password allowed. |
|
|
114
|
+
| `max_failed_login_attempts` | `5` | Failed sign-ins before an account is temporarily locked. |
|
|
115
|
+
| `lockout_duration` | `1800` | How long an account stays locked, in seconds. |
|
|
116
|
+
| `customer_password_reset_expires_in` | `15` | How long a customer's password reset link stays valid, in minutes. |
|
|
117
|
+
| `admin_password_reset_expires_in` | `15` | How long a staff password reset link stays valid, in minutes. |
|
|
118
|
+
|
|
119
|
+
## URLs
|
|
120
|
+
|
|
121
|
+
| Setting | Default | What it does |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| `dashboard_url` | `nil` | Where the React dashboard lives, so single sign-on can send staff back to it after they authenticate. |
|
|
124
|
+
| `admin_url` | `nil` | Base address used to build staff invitation links in email. Invitation links are relative when this is unset. |
|
|
125
|
+
|
|
126
|
+
## Imports and events
|
|
127
|
+
|
|
128
|
+
| Setting | Default | What it does |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| `large_import_threshold` | `500` | Row count above which a CSV import switches to bulk processing — faster, with less live progress detail. |
|
|
131
|
+
| `events_log_enabled` | `true` | Writes every Spree event to the Rails log. |
|
|
132
|
+
| `disable_migration_check` | `false` | Silences the boot warning about pending engine migrations. |
|
|
133
|
+
|
|
134
|
+
## API settings
|
|
135
|
+
|
|
136
|
+
API settings live on their own configuration object:
|
|
137
|
+
|
|
138
|
+
```ruby config/initializers/spree.rb
|
|
139
|
+
Spree::Api::Config.jwt_expiration = 1800
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Authentication
|
|
143
|
+
|
|
144
|
+
| Setting | Default | What it does |
|
|
145
|
+
|---|---|---|
|
|
146
|
+
| `jwt_expiration` | `3600` | How long a customer or store access token stays valid, in seconds. |
|
|
147
|
+
| `admin_jwt_expiration` | `300` | How long a staff access token stays valid. Deliberately short — admin tokens carry far more privilege. |
|
|
148
|
+
| `refresh_token_expiry` | `2592000` | How long a refresh token stays valid, in seconds (30 days). |
|
|
149
|
+
| `jwt_secret_key` | `nil` | Signing key for tokens. Falls back to the Rails secret key base. |
|
|
150
|
+
|
|
151
|
+
### Rate limits
|
|
152
|
+
|
|
153
|
+
| Setting | Default | What it does |
|
|
154
|
+
|---|---|---|
|
|
155
|
+
| `rate_limit_window` | `60` | Length of the rate-limit window, in seconds. |
|
|
156
|
+
| `rate_limit_per_key` | `300` | Requests allowed per window for each publishable API key + client IP pair (per visitor), and for keyless traffic per IP. Applies across the whole API, not per endpoint. |
|
|
157
|
+
| `rate_limit_per_secret_key` | `600` | Requests allowed per secret API key per window, across the whole API. |
|
|
158
|
+
| `rate_limit_login` | `5` | Sign-in attempts allowed per IP per window. |
|
|
159
|
+
| `rate_limit_register` | `3` | Registration attempts per IP. |
|
|
160
|
+
| `rate_limit_refresh` | `10` | Token refresh attempts per IP. |
|
|
161
|
+
| `rate_limit_password_reset` | `3` | Password reset requests per IP. |
|
|
162
|
+
| `max_request_body_size` | `102400` | Largest accepted request body, in bytes (100 KB). |
|
|
163
|
+
|
|
164
|
+
### Webhooks
|
|
165
|
+
|
|
166
|
+
| Setting | Default | What it does |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| `webhooks_enabled` | `true` | Whether webhooks fire at all. |
|
|
169
|
+
| `webhooks_verify_ssl` | `true` | Verifies SSL certificates on webhook endpoints. Off in development. |
|
|
170
|
+
|
|
171
|
+
## Reading a setting
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
Spree::Config.minimum_password_length # => 8
|
|
175
|
+
Spree::Api::Config.jwt_expiration # => 3600
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Assigning at runtime affects only the current process and is lost on restart. Set values in the initializer to make them stick.
|
|
179
|
+
|
|
180
|
+
## Deprecated settings
|
|
181
|
+
|
|
182
|
+
These still exist so upgrades don't break, but Spree 6 either ignores them or reads them only from subsystems it has replaced. Don't use them in new code.
|
|
183
|
+
|
|
184
|
+
| Setting | Use instead |
|
|
185
|
+
|---|---|
|
|
186
|
+
| `return_eligibility_number_of_days` | The `return_window_days` preference on [Market](../core-concepts/markets.md), or a `returns.create.validate` [hook](workflows.md) |
|
|
187
|
+
| `restock_inventory` | Restocking is decided per return line item by its resellable flag — see [Returns](../core-concepts/returns-exchanges-claims.md#processing-a-return) |
|
|
188
|
+
| `expedited_exchanges` | [Exchanges](../core-concepts/returns-exchanges-claims.md) |
|
|
189
|
+
| `expedited_exchanges_days_window` | [Exchanges](../core-concepts/returns-exchanges-claims.md) |
|
|
190
|
+
| `enable_legacy_default_price` | Compatibility switch for the old default-price behavior; leave off |
|
|
191
|
+
| `use_user_locale` | Locale comes from the `x-spree-locale` header or the `locale` parameter |
|
|
192
|
+
| `company` | The `company_field_enabled` setting on the store |
|
|
193
|
+
| `address_requires_phone` | The `address_requires_phone` setting on the store |
|
|
194
|
+
| `default_stock_reservation_ttl_minutes` | The `stock_reservation_ttl_minutes` setting on the store |
|
|
195
|
+
| `auto_capture` | The `capture_method` setting on the store |
|
|
196
|
+
| `auto_capture_on_dispatch` | The `capture_method` setting on the store |
|
|
197
|
+
| `stock_reservations_enabled` | The `stock_reservations_enabled` setting on the store |
|
|
198
|
+
| `track_inventory_levels` | The `track_inventory_levels` setting on the store |
|
|
199
|
+
| `show_products_without_price` | The `show_products_without_price` setting on the store |
|
|
200
|
+
| `disable_sku_validation` | The `disable_sku_validation` setting on the store |
|
|
201
|
+
| `track_price_history` | The `track_price_history` setting on the store |
|
|
202
|
+
| `tax_using_ship_address` | The `tax_using_ship_address` setting on the store |
|
|
203
|
+
| `disable_store_presence_validation` | Every store-owned record now requires a store |
|
|
204
|
+
|
|
205
|
+
> **WARNING:** These settings have no effect at all in Spree 6, and are scheduled for removal: `allow_checkout_on_gateway_error` (completion checks whether payments cover the total, so a failed gateway call never completes an order), `products_per_page`, `alternative_shipping_phone`, `show_variant_full_price`, `reserve_stock_on`, `storefront_products_path`, `storefront_taxons_path`, `storefront_pages_path`. Setting them changes nothing.
|
|
206
|
+
|
|
207
|
+
## Settings removed in Spree 6
|
|
208
|
+
|
|
209
|
+
These were deprecated in Spree 5 and are gone. Assigning one in your initializer now raises `NoMethodError`, so delete the line.
|
|
210
|
+
|
|
211
|
+
| Setting | What to do instead |
|
|
212
|
+
|---|---|
|
|
213
|
+
| `send_core_emails` | The `send_consumer_transactional_emails` setting on the store, which turns a shop's customer emails on and off. |
|
|
214
|
+
| `currency` | Currency belongs to the store and the market. |
|
|
215
|
+
| `address_requires_state` | State requirements come from the address country. |
|
|
216
|
+
| `checkout_zone` | Use [delivery zones](../core-concepts/fulfillments.md#delivery-methods-and-zones). |
|
|
217
|
+
| `allow_guest_checkout` | Use the `guest_checkout` setting on the store or channel. |
|
|
218
|
+
| `binary_inventory_cache` | A stock change always touches its variant. |
|
|
219
|
+
| `logo`, `mailer_logo` | The store's own `logo` and `mailer_logo` images, uploaded in the dashboard. Only the global settings are gone; the images themselves are unchanged. |
|
|
220
|
+
| `layout`, `always_put_site_name_in_title`, `title_site_name_separator`, `max_level_in_taxons_menu`, `show_raw_product_description`, `shipping_instructions`, `show_only_complete_orders_by_default` | These configured the removed Rails storefront and admin. A storefront owns its own presentation. |
|
|
221
|
+
|
|
222
|
+
## Related
|
|
223
|
+
|
|
224
|
+
- [Model preferences](model-preferences.md) — per-record settings
|
|
225
|
+
- [Services & Workflows](workflows.md) — changing behavior rather than settings
|
|
@@ -8,9 +8,9 @@ description: Use Spree decorators to add or modify behavior on core models, cont
|
|
|
8
8
|
> - **[Events & Subscribers](../core-concepts/events.md)** - For reacting to model changes (after save, create, update, delete)
|
|
9
9
|
> - **[Webhooks](../core-concepts/webhooks.md)** - For notifying external services when events occur
|
|
10
10
|
> - **[Dependencies](dependencies.md)** - For swapping out services, serializers, and abilities
|
|
11
|
-
> - **[Admin Navigation](../
|
|
12
|
-
> - **[Admin Partials](../
|
|
13
|
-
> - **[Admin Tables](../
|
|
11
|
+
> - **[Admin Navigation](../dashboard/customization/navigation.md)** - For adding menu items without controller decorators
|
|
12
|
+
> - **[Admin Partials](../dashboard/customization/quickstart.md)** - For extending admin UI without view decorators
|
|
13
|
+
> - **[Admin Tables](../dashboard/customization/tables.md)** - For customizing admin list views
|
|
14
14
|
> - **[Ransack Configuration](../core-concepts/search-filtering.md#extending-ransackable-configuration)** - For adding searchable/sortable fields without model decorators
|
|
15
15
|
|
|
16
16
|
## When to Use Decorators vs Modern Alternatives
|
|
@@ -23,8 +23,8 @@ Before reaching for a decorator, check if your use case is better served by a mo
|
|
|
23
23
|
| Notify external service on changes | Model decorator with callbacks | [Webhooks](../core-concepts/webhooks.md) |
|
|
24
24
|
| Custom add-to-cart logic | Service decorator | [Dependencies injection](dependencies.md) |
|
|
25
25
|
| Custom API responses | Serializer decorator | [Dependencies injection](dependencies.md) |
|
|
26
|
-
| Add admin menu item | Controller decorator | [Admin Navigation API](../
|
|
27
|
-
| Add section to admin form | View decorator/override | [Admin Partials injection](../
|
|
26
|
+
| Add admin menu item | Controller decorator | [Admin Navigation API](../dashboard/customization/navigation.md) |
|
|
27
|
+
| Add section to admin form | View decorator/override | [Admin Partials injection](../dashboard/customization/quickstart.md) |
|
|
28
28
|
| Add searchable/filterable field | Model decorator with `ransackable_attributes` | [Ransack configuration](../core-concepts/search-filtering.md#extending-ransackable-configuration) |
|
|
29
29
|
| Add association to core model | - | Decorator (still appropriate) |
|
|
30
30
|
| Add validation to core model | - | Decorator (still appropriate) |
|
|
@@ -528,7 +528,7 @@ end
|
|
|
528
528
|
- [Events](../core-concepts/events.md) - Learn about Spree's event system
|
|
529
529
|
- [Webhooks](../core-concepts/webhooks.md) - HTTP callbacks for external integrations
|
|
530
530
|
- [Dependencies](dependencies.md) - Swap core services with your own
|
|
531
|
-
- [Admin Navigation](../
|
|
532
|
-
- [Admin Partials](../
|
|
533
|
-
- [Extending Core Models Tutorial](../tutorial/
|
|
531
|
+
- [Admin Navigation](../dashboard/customization/navigation.md) - Extend admin menu without decorators
|
|
532
|
+
- [Admin Partials](../dashboard/customization/quickstart.md) - Extend admin UI without view decorators
|
|
533
|
+
- [Extending Core Models Tutorial](../tutorial/model.md) - Step-by-step guide to connecting custom models with Spree core
|
|
534
534
|
- [Customization Overview](quickstart.md) - General customization patterns
|