@spree/docs 0.1.0
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/README.md +54 -0
- package/dist/api-reference/platform/authentication.md +38 -0
- package/dist/api-reference/store-api/authentication.md +188 -0
- package/dist/api-reference/store-api/errors.md +277 -0
- package/dist/api-reference/store-api/idempotency.md +129 -0
- package/dist/api-reference/store-api/introduction.md +34 -0
- package/dist/api-reference/store-api/localization.md +279 -0
- package/dist/api-reference/store-api/metadata.md +160 -0
- package/dist/api-reference/store-api/monetary-amounts.md +65 -0
- package/dist/api-reference/store-api/querying.md +399 -0
- package/dist/api-reference/store-api/rate-limitting.md +103 -0
- package/dist/api-reference/store-api/relations.md +185 -0
- package/dist/api-reference/storefront/authentication.md +88 -0
- package/dist/api-reference/tutorials/adyen-integration-guide-for-android.md +165 -0
- package/dist/api-reference/tutorials/adyen-integration-guide-for-ios.md +194 -0
- package/dist/api-reference/tutorials/quick-checkout-with-stripe.md +248 -0
- package/dist/api-reference/v2/fetching-multiple-resources.md +26 -0
- package/dist/api-reference/v2/filtering-and-sorting.md +53 -0
- package/dist/api-reference/v2/introduction.md +22 -0
- package/dist/api-reference/v2/pagination.md +37 -0
- package/dist/api-reference/webhooks-events.md +883 -0
- package/dist/developer/admin/admin.md +205 -0
- package/dist/developer/admin/authentication.md +59 -0
- package/dist/developer/admin/components.md +711 -0
- package/dist/developer/admin/custom-css.md +243 -0
- package/dist/developer/admin/custom-javascript.md +116 -0
- package/dist/developer/admin/extending-ui.md +1964 -0
- package/dist/developer/admin/form-builder.md +444 -0
- package/dist/developer/admin/helper-methods.md +531 -0
- package/dist/developer/admin/navigation.md +805 -0
- package/dist/developer/admin/tables.md +491 -0
- package/dist/developer/advanced/adding_spree_to_rails_app.md +106 -0
- package/dist/developer/cli/quickstart.md +137 -0
- package/dist/developer/contributing/creating-an-extension.md +258 -0
- package/dist/developer/contributing/developing-spree.md +339 -0
- package/dist/developer/contributing/quickstart.md +32 -0
- package/dist/developer/contributing/updating-extensions.md +67 -0
- package/dist/developer/core-concepts/addresses.md +265 -0
- package/dist/developer/core-concepts/adjustments.md +107 -0
- package/dist/developer/core-concepts/architecture.md +177 -0
- package/dist/developer/core-concepts/calculators.md +323 -0
- package/dist/developer/core-concepts/customers.md +230 -0
- package/dist/developer/core-concepts/events.md +624 -0
- package/dist/developer/core-concepts/imports-exports.md +698 -0
- package/dist/developer/core-concepts/inventory.md +191 -0
- package/dist/developer/core-concepts/markets.md +250 -0
- package/dist/developer/core-concepts/media.md +167 -0
- package/dist/developer/core-concepts/metafields.md +187 -0
- package/dist/developer/core-concepts/orders.md +328 -0
- package/dist/developer/core-concepts/payments.md +710 -0
- package/dist/developer/core-concepts/pricing.md +163 -0
- package/dist/developer/core-concepts/products.md +360 -0
- package/dist/developer/core-concepts/promotions.md +322 -0
- package/dist/developer/core-concepts/reports.md +206 -0
- package/dist/developer/core-concepts/search-filtering.md +237 -0
- package/dist/developer/core-concepts/shipments.md +212 -0
- package/dist/developer/core-concepts/slugs.md +111 -0
- package/dist/developer/core-concepts/staff-roles.md +123 -0
- package/dist/developer/core-concepts/store-credits-gift-cards.md +317 -0
- package/dist/developer/core-concepts/stores.md +117 -0
- package/dist/developer/core-concepts/taxes.md +135 -0
- package/dist/developer/core-concepts/translations.md +120 -0
- package/dist/developer/core-concepts/users.md +299 -0
- package/dist/developer/core-concepts/webhooks.md +378 -0
- package/dist/developer/create-spree-app/quickstart.md +158 -0
- package/dist/developer/customization/api.md +93 -0
- package/dist/developer/customization/authentication.md +88 -0
- package/dist/developer/customization/checkout.md +204 -0
- package/dist/developer/customization/configuration.md +55 -0
- package/dist/developer/customization/decorators.md +523 -0
- package/dist/developer/customization/dependencies.md +232 -0
- package/dist/developer/customization/emails.md +21 -0
- package/dist/developer/customization/extensions.md +92 -0
- package/dist/developer/customization/metadata.md +236 -0
- package/dist/developer/customization/model-preferences.md +130 -0
- package/dist/developer/customization/permissions.md +265 -0
- package/dist/developer/customization/quickstart.md +229 -0
- package/dist/developer/customization/routes.md +24 -0
- package/dist/developer/customization/v4/admin-panel.md +78 -0
- package/dist/developer/customization/v4/authentication.md +210 -0
- package/dist/developer/customization/v4/checkout.md +212 -0
- package/dist/developer/customization/v4/deface.md +251 -0
- package/dist/developer/customization/v4/images.md +86 -0
- package/dist/developer/customization/v4/storefront.md +450 -0
- package/dist/developer/deployment/assets.md +87 -0
- package/dist/developer/deployment/aws.md +335 -0
- package/dist/developer/deployment/caching.md +27 -0
- package/dist/developer/deployment/cdn.md +39 -0
- package/dist/developer/deployment/database.md +155 -0
- package/dist/developer/deployment/docker.md +128 -0
- package/dist/developer/deployment/emails.md +77 -0
- package/dist/developer/deployment/environment_variables.md +111 -0
- package/dist/developer/deployment/heroku.md +51 -0
- package/dist/developer/deployment/render.md +95 -0
- package/dist/developer/getting-started/quickstart.md +82 -0
- package/dist/developer/how-to/custom-payment-method.md +374 -0
- package/dist/developer/how-to/custom-promotion.md +373 -0
- package/dist/developer/how-to/custom-report.md +387 -0
- package/dist/developer/how-to/custom-search-provider.md +230 -0
- package/dist/developer/multi-store/quickstart.md +71 -0
- package/dist/developer/multi-store/setup.md +38 -0
- package/dist/developer/multi-tenant/configuration.md +41 -0
- package/dist/developer/multi-tenant/core-concepts.md +75 -0
- package/dist/developer/multi-tenant/installation.md +96 -0
- package/dist/developer/multi-tenant/quickstart.md +20 -0
- package/dist/developer/multi-vendor/installation.md +45 -0
- package/dist/developer/multi-vendor/quickstart.md +17 -0
- package/dist/developer/sdk/admin/quickstart.md +22 -0
- package/dist/developer/sdk/authentication.md +89 -0
- package/dist/developer/sdk/configuration.md +225 -0
- package/dist/developer/sdk/quickstart.md +82 -0
- package/dist/developer/sdk/store/account.md +67 -0
- package/dist/developer/sdk/store/cart-checkout.md +140 -0
- package/dist/developer/sdk/store/markets.md +151 -0
- package/dist/developer/sdk/store/payments.md +96 -0
- package/dist/developer/sdk/store/products.md +149 -0
- package/dist/developer/sdk/store/wishlists.md +52 -0
- package/dist/developer/security/pci_compliance.md +15 -0
- package/dist/developer/security/security_policy.md +68 -0
- package/dist/developer/storefront/blocks.md +285 -0
- package/dist/developer/storefront/custom-css.md +260 -0
- package/dist/developer/storefront/custom-javascript.md +166 -0
- package/dist/developer/storefront/helper-methods.md +1288 -0
- package/dist/developer/storefront/links.md +298 -0
- package/dist/developer/storefront/nextjs/architecture.md +150 -0
- package/dist/developer/storefront/nextjs/customization.md +141 -0
- package/dist/developer/storefront/nextjs/deployment.md +180 -0
- package/dist/developer/storefront/nextjs/quickstart.md +92 -0
- package/dist/developer/storefront/nextjs/spree-next-package.md +314 -0
- package/dist/developer/storefront/pages.md +163 -0
- package/dist/developer/storefront/sections.md +569 -0
- package/dist/developer/storefront/storefront.md +56 -0
- package/dist/developer/storefront/themes.md +161 -0
- package/dist/developer/tutorial/admin.md +134 -0
- package/dist/developer/tutorial/extending-models.md +380 -0
- package/dist/developer/tutorial/file-uploads.md +121 -0
- package/dist/developer/tutorial/introduction.md +33 -0
- package/dist/developer/tutorial/model.md +41 -0
- package/dist/developer/tutorial/page-builder.md +487 -0
- package/dist/developer/tutorial/rich-text.md +73 -0
- package/dist/developer/tutorial/seo.md +332 -0
- package/dist/developer/tutorial/storefront.md +352 -0
- package/dist/developer/tutorial/testing.md +558 -0
- package/dist/developer/upgrades/2.0-to-2.1.md +46 -0
- package/dist/developer/upgrades/2.1-to-2.2.md +59 -0
- package/dist/developer/upgrades/2.2-to-2.3.md +44 -0
- package/dist/developer/upgrades/2.3-to-2.4.md +42 -0
- package/dist/developer/upgrades/3.0-to-3.1.md +47 -0
- package/dist/developer/upgrades/3.1-to-3.2.md +34 -0
- package/dist/developer/upgrades/3.2-to-3.3.md +70 -0
- package/dist/developer/upgrades/3.3-to-3.4.md +36 -0
- package/dist/developer/upgrades/3.4-to-3.5.md +44 -0
- package/dist/developer/upgrades/3.5-to-3.6.md +40 -0
- package/dist/developer/upgrades/3.6-to-3.7.md +62 -0
- package/dist/developer/upgrades/3.7-to-4.0.md +152 -0
- package/dist/developer/upgrades/4.0-to-4.1.md +92 -0
- package/dist/developer/upgrades/4.1-to-4.2.md +109 -0
- package/dist/developer/upgrades/4.10-to-5.0.md +129 -0
- package/dist/developer/upgrades/4.2-to-4.3.md +100 -0
- package/dist/developer/upgrades/4.3-to-4.4.md +125 -0
- package/dist/developer/upgrades/4.4-to-4.5.md +94 -0
- package/dist/developer/upgrades/4.5-to-4.6.md +119 -0
- package/dist/developer/upgrades/4.6-to-4.7.md +39 -0
- package/dist/developer/upgrades/4.8-to-4.9.md +24 -0
- package/dist/developer/upgrades/4.9-to-4.10.md +24 -0
- package/dist/developer/upgrades/4.x-to-4.8.md +52 -0
- package/dist/developer/upgrades/5.0-to-5.1.md +28 -0
- package/dist/developer/upgrades/5.1-to-5.2.md +127 -0
- package/dist/developer/upgrades/5.2-to-5.3.md +338 -0
- package/dist/developer/upgrades/5.3-to-5.4.md +248 -0
- package/dist/developer/upgrades/quickstart.md +36 -0
- package/dist/integrations/analytics/google-analytics.md +64 -0
- package/dist/integrations/analytics/google-tag-manager.md +78 -0
- package/dist/integrations/integrations.md +39 -0
- package/dist/integrations/marketing/klaviyo.md +99 -0
- package/dist/integrations/payments/adyen.md +90 -0
- package/dist/integrations/payments/paypal.md +41 -0
- package/dist/integrations/payments/razorpay.md +45 -0
- package/dist/integrations/payments/stripe.md +109 -0
- package/dist/integrations/search/meilisearch.md +236 -0
- package/dist/integrations/sso-mfa-social-login/admin-dashboard.md +57 -0
- package/dist/integrations/sso-mfa-social-login/storefront.md +56 -0
- package/package.json +27 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Build Custom Promotion Rules & Actions
|
|
3
|
+
description: Step-by-step guide to creating custom promotion rules and actions for business-specific eligibility logic and discount behaviors.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
Spree's promotion system is built around two extension points: **Rules** (eligibility conditions) and **Actions** (what happens when a promotion applies). While Spree ships with a comprehensive set of [built-in rules and actions](/developer/core-concepts/promotions#rules), you can create custom ones for business-specific logic.
|
|
9
|
+
|
|
10
|
+
This guide covers:
|
|
11
|
+
- Creating a custom promotion rule with admin UI
|
|
12
|
+
- Creating a custom promotion action with a calculator
|
|
13
|
+
- Understanding the `eligible?`, `actionable?`, and `perform` contracts
|
|
14
|
+
|
|
15
|
+
Before starting, make sure you understand [how promotions work in Spree](/developer/core-concepts/promotions).
|
|
16
|
+
|
|
17
|
+
## Custom Promotion Rules
|
|
18
|
+
|
|
19
|
+
Rules determine whether a promotion is eligible for a given order. Each rule implements `eligible?` which returns `true` or `false`.
|
|
20
|
+
|
|
21
|
+
### Step 1: Create the Rule Class
|
|
22
|
+
|
|
23
|
+
Create a new class inheriting from `Spree::PromotionRule`:
|
|
24
|
+
|
|
25
|
+
```ruby app/models/spree/promotion/rules/minimum_quantity.rb
|
|
26
|
+
module Spree
|
|
27
|
+
class Promotion
|
|
28
|
+
module Rules
|
|
29
|
+
class MinimumQuantity < Spree::PromotionRule
|
|
30
|
+
preference :quantity, :integer, default: 5
|
|
31
|
+
|
|
32
|
+
def applicable?(promotable)
|
|
33
|
+
promotable.is_a?(Spree::Order)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def eligible?(order, options = {})
|
|
37
|
+
total_quantity = order.line_items.sum(&:quantity)
|
|
38
|
+
|
|
39
|
+
if total_quantity >= preferred_quantity
|
|
40
|
+
true
|
|
41
|
+
else
|
|
42
|
+
eligibility_errors.add(
|
|
43
|
+
:base,
|
|
44
|
+
"Order must contain at least #{preferred_quantity} items"
|
|
45
|
+
)
|
|
46
|
+
false
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
#### Key Methods to Implement
|
|
56
|
+
|
|
57
|
+
| Method | Required | Description |
|
|
58
|
+
|--------|----------|-------------|
|
|
59
|
+
| `applicable?(promotable)` | Yes | Returns `true` if this rule type can evaluate the promotable (usually `promotable.is_a?(Spree::Order)`) |
|
|
60
|
+
| `eligible?(promotable, options = {})` | Yes | Returns `true` if the promotable meets this rule's conditions. Add messages to `eligibility_errors` to explain why not. |
|
|
61
|
+
| `actionable?(line_item)` | No | Returns `true` if a specific line item should receive the promotion's action. Defaults to `true`. Override this for rules that target specific items (like product or category rules). |
|
|
62
|
+
|
|
63
|
+
The `options` hash passed to `eligible?` can include `:user`, `:email`, and other context from the checkout flow.
|
|
64
|
+
|
|
65
|
+
#### Using Preferences
|
|
66
|
+
|
|
67
|
+
Rules use Spree's preference system for configuration. Each preference creates getter/setter methods automatically:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
preference :amount, :decimal, default: 100.00
|
|
71
|
+
preference :operator, :string, default: 'gte'
|
|
72
|
+
preference :category_ids, :array, default: []
|
|
73
|
+
|
|
74
|
+
# These create:
|
|
75
|
+
# preferred_amount / preferred_amount=
|
|
76
|
+
# preferred_operator / preferred_operator=
|
|
77
|
+
# preferred_category_ids / preferred_category_ids=
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Available types: `:string`, `:integer`, `:decimal`, `:boolean`, `:array`.
|
|
81
|
+
|
|
82
|
+
### Step 2: Register the Rule
|
|
83
|
+
|
|
84
|
+
Add your rule to the promotion configuration so it appears in the admin panel:
|
|
85
|
+
|
|
86
|
+
```ruby config/initializers/spree.rb
|
|
87
|
+
Rails.application.config.after_initialize do
|
|
88
|
+
Spree.promotions.rules << Spree::Promotion::Rules::MinimumQuantity
|
|
89
|
+
end
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Step 3: Create the Admin Partial
|
|
93
|
+
|
|
94
|
+
Create a form partial so admins can configure the rule's preferences. The partial name must match the rule class name in underscore format:
|
|
95
|
+
|
|
96
|
+
```erb app/views/spree/admin/promotions/rules/_minimum_quantity.html.erb
|
|
97
|
+
<div class="row mb-3">
|
|
98
|
+
<%= f.spree_number_field :preferred_quantity, label: Spree.t(:minimum_quantity) %>
|
|
99
|
+
</div>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Step 4: Add Translations
|
|
103
|
+
|
|
104
|
+
```yaml config/locales/en.yml
|
|
105
|
+
en:
|
|
106
|
+
spree:
|
|
107
|
+
minimum_quantity: Minimum Quantity
|
|
108
|
+
promotion_rule_types:
|
|
109
|
+
minimum_quantity:
|
|
110
|
+
name: Minimum Quantity
|
|
111
|
+
description: Order must contain at least X items
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Step 5: Restart and Test
|
|
115
|
+
|
|
116
|
+
After restarting your application, the new rule will be available in **Admin > Promotions** when adding rules to a promotion.
|
|
117
|
+
|
|
118
|
+
### Example: Rule with `actionable?`
|
|
119
|
+
|
|
120
|
+
When your rule targets specific line items (not the whole order), implement `actionable?` so that actions like `CreateItemAdjustments` only discount matching items:
|
|
121
|
+
|
|
122
|
+
```ruby app/models/spree/promotion/rules/brand.rb
|
|
123
|
+
module Spree
|
|
124
|
+
class Promotion
|
|
125
|
+
module Rules
|
|
126
|
+
class Brand < Spree::PromotionRule
|
|
127
|
+
preference :brand_names, :array, default: []
|
|
128
|
+
|
|
129
|
+
def applicable?(promotable)
|
|
130
|
+
promotable.is_a?(Spree::Order)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def eligible?(order, options = {})
|
|
134
|
+
order.line_items.any? { |li| matches_brand?(li) }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Only discount line items from matching brands
|
|
138
|
+
def actionable?(line_item)
|
|
139
|
+
matches_brand?(line_item)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
private
|
|
143
|
+
|
|
144
|
+
def matches_brand?(line_item)
|
|
145
|
+
brand = line_item.product.get_metafield('details.brand')&.value
|
|
146
|
+
preferred_brand_names.include?(brand)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Custom Promotion Actions
|
|
155
|
+
|
|
156
|
+
Actions define what happens when a promotion is applied. Most actions create [adjustments](/developer/core-concepts/adjustments) on orders or line items.
|
|
157
|
+
|
|
158
|
+
### Step 1: Create the Action Class
|
|
159
|
+
|
|
160
|
+
#### Discount Action (with Calculator)
|
|
161
|
+
|
|
162
|
+
For actions that create monetary adjustments, include `Spree::CalculatedAdjustments` and `Spree::AdjustmentSource`:
|
|
163
|
+
|
|
164
|
+
```ruby app/models/spree/promotion/actions/tiered_discount.rb
|
|
165
|
+
module Spree
|
|
166
|
+
class Promotion
|
|
167
|
+
module Actions
|
|
168
|
+
class TieredDiscount < Spree::PromotionAction
|
|
169
|
+
include Spree::CalculatedAdjustments
|
|
170
|
+
include Spree::AdjustmentSource
|
|
171
|
+
|
|
172
|
+
before_validation -> { self.calculator ||= Calculator::FlatRate.new }
|
|
173
|
+
|
|
174
|
+
def perform(options = {})
|
|
175
|
+
order = options[:order]
|
|
176
|
+
return false unless order.present?
|
|
177
|
+
|
|
178
|
+
create_unique_adjustment(order, order)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def compute_amount(order)
|
|
182
|
+
# Tiered discount: $10 off orders over $50, $25 off orders over $100
|
|
183
|
+
discount = case order.item_total
|
|
184
|
+
when 100..Float::INFINITY then 25
|
|
185
|
+
when 50..99.99 then 10
|
|
186
|
+
else 0
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Must return negative amount for discounts
|
|
190
|
+
# Cap at order total to prevent negative orders
|
|
191
|
+
[discount, order.item_total].min * -1
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
#### Non-Discount Action
|
|
200
|
+
|
|
201
|
+
For actions that don't create adjustments (e.g., awarding points, sending notifications):
|
|
202
|
+
|
|
203
|
+
```ruby app/models/spree/promotion/actions/add_loyalty_points.rb
|
|
204
|
+
module Spree
|
|
205
|
+
class Promotion
|
|
206
|
+
module Actions
|
|
207
|
+
class AddLoyaltyPoints < Spree::PromotionAction
|
|
208
|
+
preference :points, :integer, default: 100
|
|
209
|
+
|
|
210
|
+
def perform(options = {})
|
|
211
|
+
order = options[:order]
|
|
212
|
+
return false unless order.user.present?
|
|
213
|
+
|
|
214
|
+
order.user.add_loyalty_points(preferred_points, source: promotion)
|
|
215
|
+
true
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
#### Key Methods to Implement
|
|
224
|
+
|
|
225
|
+
| Method | Required | Description |
|
|
226
|
+
|--------|----------|-------------|
|
|
227
|
+
| `perform(options = {})` | Yes | Called when the promotion is activated. `options` includes `:order` and `:promotion`. Return `true` if the action was applied. |
|
|
228
|
+
| `compute_amount(adjustable)` | For discount actions | Return the adjustment amount (negative for discounts). Cap at the adjustable's total to prevent negative amounts. |
|
|
229
|
+
| `revert(options = {})` | No | Called when a promotion is deactivated. Use to undo side effects (e.g., remove added line items). |
|
|
230
|
+
|
|
231
|
+
#### Available Helper Methods
|
|
232
|
+
|
|
233
|
+
When you include `Spree::AdjustmentSource`, you get:
|
|
234
|
+
|
|
235
|
+
```ruby
|
|
236
|
+
# Create a single adjustment (e.g., on the order)
|
|
237
|
+
create_unique_adjustment(order, adjustable)
|
|
238
|
+
|
|
239
|
+
# Create adjustments on multiple items (e.g., all line items)
|
|
240
|
+
create_unique_adjustments(order, order.line_items)
|
|
241
|
+
|
|
242
|
+
# With a filter block (e.g., only actionable line items)
|
|
243
|
+
create_unique_adjustments(order, order.line_items) do |line_item|
|
|
244
|
+
promotion.line_item_actionable?(order, line_item)
|
|
245
|
+
end
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
When you include `Spree::CalculatedAdjustments`, you get:
|
|
249
|
+
|
|
250
|
+
```ruby
|
|
251
|
+
# Delegate to the calculator
|
|
252
|
+
compute(adjustable) # calls calculator.compute(adjustable)
|
|
253
|
+
|
|
254
|
+
# Set calculator by class name
|
|
255
|
+
self.calculator_type = 'Spree::Calculator::FlatRate'
|
|
256
|
+
|
|
257
|
+
# List available calculators for this action type
|
|
258
|
+
self.class.calculators
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Step 2: Register the Action
|
|
262
|
+
|
|
263
|
+
```ruby config/initializers/spree.rb
|
|
264
|
+
Rails.application.config.after_initialize do
|
|
265
|
+
Spree.promotions.actions << Spree::Promotion::Actions::TieredDiscount
|
|
266
|
+
end
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Step 3: Add Translations
|
|
270
|
+
|
|
271
|
+
```yaml config/locales/en.yml
|
|
272
|
+
en:
|
|
273
|
+
spree:
|
|
274
|
+
promotion_action_types:
|
|
275
|
+
tiered_discount:
|
|
276
|
+
name: Tiered Discount
|
|
277
|
+
description: Different discount amounts based on order total tiers
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Step 4: Restart and Test
|
|
281
|
+
|
|
282
|
+
After restarting, the new action will be available in **Admin > Promotions** when adding actions to a promotion.
|
|
283
|
+
|
|
284
|
+
## How Rules and Actions Work Together
|
|
285
|
+
|
|
286
|
+
Understanding how Spree evaluates promotions helps you build better custom rules and actions:
|
|
287
|
+
|
|
288
|
+
```mermaid
|
|
289
|
+
flowchart TD
|
|
290
|
+
A[Order Updated] --> B[PromotionHandler::Cart]
|
|
291
|
+
B --> C{For each promotion}
|
|
292
|
+
C --> D{Check dates & usage}
|
|
293
|
+
D -->|expired/exceeded| E[Deactivate]
|
|
294
|
+
D -->|valid| F{Evaluate rules}
|
|
295
|
+
F -->|match_policy: all| G[ALL rules must pass]
|
|
296
|
+
F -->|match_policy: any| H[ANY rule must pass]
|
|
297
|
+
G -->|eligible| I[Run actions]
|
|
298
|
+
H -->|eligible| I
|
|
299
|
+
G -->|ineligible| E
|
|
300
|
+
H -->|ineligible| E
|
|
301
|
+
I --> J[action.perform for each action]
|
|
302
|
+
J --> K[Create adjustments]
|
|
303
|
+
K --> L[Best promotion wins]
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**Key points:**
|
|
307
|
+
- `match_policy: 'all'` means every rule must return `eligible? == true`
|
|
308
|
+
- `match_policy: 'any'` means at least one rule must return `eligible? == true`
|
|
309
|
+
- For item-level actions (`CreateItemAdjustments`), `actionable?(line_item)` on each rule filters which line items get the discount
|
|
310
|
+
- When multiple promotions compete, Spree picks the best one (largest discount) and marks others as ineligible
|
|
311
|
+
|
|
312
|
+
## Testing Custom Rules and Actions
|
|
313
|
+
|
|
314
|
+
```ruby spec/models/spree/promotion/rules/minimum_quantity_spec.rb
|
|
315
|
+
require 'spec_helper'
|
|
316
|
+
|
|
317
|
+
RSpec.describe Spree::Promotion::Rules::MinimumQuantity do
|
|
318
|
+
let(:rule) { described_class.new(preferred_quantity: 3) }
|
|
319
|
+
let(:order) { create(:order_with_line_items, line_items_count: 1) }
|
|
320
|
+
|
|
321
|
+
describe '#eligible?' do
|
|
322
|
+
context 'when order has enough items' do
|
|
323
|
+
before { order.line_items.first.update(quantity: 3) }
|
|
324
|
+
|
|
325
|
+
it { expect(rule.eligible?(order)).to be true }
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
context 'when order does not have enough items' do
|
|
329
|
+
it { expect(rule.eligible?(order)).to be false }
|
|
330
|
+
|
|
331
|
+
it 'sets eligibility error' do
|
|
332
|
+
rule.eligible?(order)
|
|
333
|
+
expect(rule.eligibility_errors.full_messages).to include(
|
|
334
|
+
/at least 3 items/
|
|
335
|
+
)
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
```ruby spec/models/spree/promotion/actions/tiered_discount_spec.rb
|
|
343
|
+
require 'spec_helper'
|
|
344
|
+
|
|
345
|
+
RSpec.describe Spree::Promotion::Actions::TieredDiscount do
|
|
346
|
+
let(:promotion) { create(:promotion) }
|
|
347
|
+
let(:action) { described_class.create!(promotion: promotion) }
|
|
348
|
+
|
|
349
|
+
describe '#compute_amount' do
|
|
350
|
+
it 'returns -10 for orders over $50' do
|
|
351
|
+
order = build(:order, item_total: 75)
|
|
352
|
+
expect(action.compute_amount(order)).to eq(-10)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
it 'returns -25 for orders over $100' do
|
|
356
|
+
order = build(:order, item_total: 150)
|
|
357
|
+
expect(action.compute_amount(order)).to eq(-25)
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
it 'returns 0 for orders under $50' do
|
|
361
|
+
order = build(:order, item_total: 30)
|
|
362
|
+
expect(action.compute_amount(order)).to eq(0)
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
## Related Documentation
|
|
369
|
+
|
|
370
|
+
- [Promotions](/developer/core-concepts/promotions) - Promotion architecture and built-in rules/actions
|
|
371
|
+
- [Calculators](/developer/core-concepts/calculators) - Available calculator types for promotion actions
|
|
372
|
+
- [Adjustments](/developer/core-concepts/adjustments) - How adjustments work on orders and line items
|
|
373
|
+
- [Events](/developer/core-concepts/events) - Subscribe to promotion events
|