@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.
Files changed (183) hide show
  1. package/README.md +54 -0
  2. package/dist/api-reference/platform/authentication.md +38 -0
  3. package/dist/api-reference/store-api/authentication.md +188 -0
  4. package/dist/api-reference/store-api/errors.md +277 -0
  5. package/dist/api-reference/store-api/idempotency.md +129 -0
  6. package/dist/api-reference/store-api/introduction.md +34 -0
  7. package/dist/api-reference/store-api/localization.md +279 -0
  8. package/dist/api-reference/store-api/metadata.md +160 -0
  9. package/dist/api-reference/store-api/monetary-amounts.md +65 -0
  10. package/dist/api-reference/store-api/querying.md +399 -0
  11. package/dist/api-reference/store-api/rate-limitting.md +103 -0
  12. package/dist/api-reference/store-api/relations.md +185 -0
  13. package/dist/api-reference/storefront/authentication.md +88 -0
  14. package/dist/api-reference/tutorials/adyen-integration-guide-for-android.md +165 -0
  15. package/dist/api-reference/tutorials/adyen-integration-guide-for-ios.md +194 -0
  16. package/dist/api-reference/tutorials/quick-checkout-with-stripe.md +248 -0
  17. package/dist/api-reference/v2/fetching-multiple-resources.md +26 -0
  18. package/dist/api-reference/v2/filtering-and-sorting.md +53 -0
  19. package/dist/api-reference/v2/introduction.md +22 -0
  20. package/dist/api-reference/v2/pagination.md +37 -0
  21. package/dist/api-reference/webhooks-events.md +883 -0
  22. package/dist/developer/admin/admin.md +205 -0
  23. package/dist/developer/admin/authentication.md +59 -0
  24. package/dist/developer/admin/components.md +711 -0
  25. package/dist/developer/admin/custom-css.md +243 -0
  26. package/dist/developer/admin/custom-javascript.md +116 -0
  27. package/dist/developer/admin/extending-ui.md +1964 -0
  28. package/dist/developer/admin/form-builder.md +444 -0
  29. package/dist/developer/admin/helper-methods.md +531 -0
  30. package/dist/developer/admin/navigation.md +805 -0
  31. package/dist/developer/admin/tables.md +491 -0
  32. package/dist/developer/advanced/adding_spree_to_rails_app.md +106 -0
  33. package/dist/developer/cli/quickstart.md +137 -0
  34. package/dist/developer/contributing/creating-an-extension.md +258 -0
  35. package/dist/developer/contributing/developing-spree.md +339 -0
  36. package/dist/developer/contributing/quickstart.md +32 -0
  37. package/dist/developer/contributing/updating-extensions.md +67 -0
  38. package/dist/developer/core-concepts/addresses.md +265 -0
  39. package/dist/developer/core-concepts/adjustments.md +107 -0
  40. package/dist/developer/core-concepts/architecture.md +177 -0
  41. package/dist/developer/core-concepts/calculators.md +323 -0
  42. package/dist/developer/core-concepts/customers.md +230 -0
  43. package/dist/developer/core-concepts/events.md +624 -0
  44. package/dist/developer/core-concepts/imports-exports.md +698 -0
  45. package/dist/developer/core-concepts/inventory.md +191 -0
  46. package/dist/developer/core-concepts/markets.md +250 -0
  47. package/dist/developer/core-concepts/media.md +167 -0
  48. package/dist/developer/core-concepts/metafields.md +187 -0
  49. package/dist/developer/core-concepts/orders.md +328 -0
  50. package/dist/developer/core-concepts/payments.md +710 -0
  51. package/dist/developer/core-concepts/pricing.md +163 -0
  52. package/dist/developer/core-concepts/products.md +360 -0
  53. package/dist/developer/core-concepts/promotions.md +322 -0
  54. package/dist/developer/core-concepts/reports.md +206 -0
  55. package/dist/developer/core-concepts/search-filtering.md +237 -0
  56. package/dist/developer/core-concepts/shipments.md +212 -0
  57. package/dist/developer/core-concepts/slugs.md +111 -0
  58. package/dist/developer/core-concepts/staff-roles.md +123 -0
  59. package/dist/developer/core-concepts/store-credits-gift-cards.md +317 -0
  60. package/dist/developer/core-concepts/stores.md +117 -0
  61. package/dist/developer/core-concepts/taxes.md +135 -0
  62. package/dist/developer/core-concepts/translations.md +120 -0
  63. package/dist/developer/core-concepts/users.md +299 -0
  64. package/dist/developer/core-concepts/webhooks.md +378 -0
  65. package/dist/developer/create-spree-app/quickstart.md +158 -0
  66. package/dist/developer/customization/api.md +93 -0
  67. package/dist/developer/customization/authentication.md +88 -0
  68. package/dist/developer/customization/checkout.md +204 -0
  69. package/dist/developer/customization/configuration.md +55 -0
  70. package/dist/developer/customization/decorators.md +523 -0
  71. package/dist/developer/customization/dependencies.md +232 -0
  72. package/dist/developer/customization/emails.md +21 -0
  73. package/dist/developer/customization/extensions.md +92 -0
  74. package/dist/developer/customization/metadata.md +236 -0
  75. package/dist/developer/customization/model-preferences.md +130 -0
  76. package/dist/developer/customization/permissions.md +265 -0
  77. package/dist/developer/customization/quickstart.md +229 -0
  78. package/dist/developer/customization/routes.md +24 -0
  79. package/dist/developer/customization/v4/admin-panel.md +78 -0
  80. package/dist/developer/customization/v4/authentication.md +210 -0
  81. package/dist/developer/customization/v4/checkout.md +212 -0
  82. package/dist/developer/customization/v4/deface.md +251 -0
  83. package/dist/developer/customization/v4/images.md +86 -0
  84. package/dist/developer/customization/v4/storefront.md +450 -0
  85. package/dist/developer/deployment/assets.md +87 -0
  86. package/dist/developer/deployment/aws.md +335 -0
  87. package/dist/developer/deployment/caching.md +27 -0
  88. package/dist/developer/deployment/cdn.md +39 -0
  89. package/dist/developer/deployment/database.md +155 -0
  90. package/dist/developer/deployment/docker.md +128 -0
  91. package/dist/developer/deployment/emails.md +77 -0
  92. package/dist/developer/deployment/environment_variables.md +111 -0
  93. package/dist/developer/deployment/heroku.md +51 -0
  94. package/dist/developer/deployment/render.md +95 -0
  95. package/dist/developer/getting-started/quickstart.md +82 -0
  96. package/dist/developer/how-to/custom-payment-method.md +374 -0
  97. package/dist/developer/how-to/custom-promotion.md +373 -0
  98. package/dist/developer/how-to/custom-report.md +387 -0
  99. package/dist/developer/how-to/custom-search-provider.md +230 -0
  100. package/dist/developer/multi-store/quickstart.md +71 -0
  101. package/dist/developer/multi-store/setup.md +38 -0
  102. package/dist/developer/multi-tenant/configuration.md +41 -0
  103. package/dist/developer/multi-tenant/core-concepts.md +75 -0
  104. package/dist/developer/multi-tenant/installation.md +96 -0
  105. package/dist/developer/multi-tenant/quickstart.md +20 -0
  106. package/dist/developer/multi-vendor/installation.md +45 -0
  107. package/dist/developer/multi-vendor/quickstart.md +17 -0
  108. package/dist/developer/sdk/admin/quickstart.md +22 -0
  109. package/dist/developer/sdk/authentication.md +89 -0
  110. package/dist/developer/sdk/configuration.md +225 -0
  111. package/dist/developer/sdk/quickstart.md +82 -0
  112. package/dist/developer/sdk/store/account.md +67 -0
  113. package/dist/developer/sdk/store/cart-checkout.md +140 -0
  114. package/dist/developer/sdk/store/markets.md +151 -0
  115. package/dist/developer/sdk/store/payments.md +96 -0
  116. package/dist/developer/sdk/store/products.md +149 -0
  117. package/dist/developer/sdk/store/wishlists.md +52 -0
  118. package/dist/developer/security/pci_compliance.md +15 -0
  119. package/dist/developer/security/security_policy.md +68 -0
  120. package/dist/developer/storefront/blocks.md +285 -0
  121. package/dist/developer/storefront/custom-css.md +260 -0
  122. package/dist/developer/storefront/custom-javascript.md +166 -0
  123. package/dist/developer/storefront/helper-methods.md +1288 -0
  124. package/dist/developer/storefront/links.md +298 -0
  125. package/dist/developer/storefront/nextjs/architecture.md +150 -0
  126. package/dist/developer/storefront/nextjs/customization.md +141 -0
  127. package/dist/developer/storefront/nextjs/deployment.md +180 -0
  128. package/dist/developer/storefront/nextjs/quickstart.md +92 -0
  129. package/dist/developer/storefront/nextjs/spree-next-package.md +314 -0
  130. package/dist/developer/storefront/pages.md +163 -0
  131. package/dist/developer/storefront/sections.md +569 -0
  132. package/dist/developer/storefront/storefront.md +56 -0
  133. package/dist/developer/storefront/themes.md +161 -0
  134. package/dist/developer/tutorial/admin.md +134 -0
  135. package/dist/developer/tutorial/extending-models.md +380 -0
  136. package/dist/developer/tutorial/file-uploads.md +121 -0
  137. package/dist/developer/tutorial/introduction.md +33 -0
  138. package/dist/developer/tutorial/model.md +41 -0
  139. package/dist/developer/tutorial/page-builder.md +487 -0
  140. package/dist/developer/tutorial/rich-text.md +73 -0
  141. package/dist/developer/tutorial/seo.md +332 -0
  142. package/dist/developer/tutorial/storefront.md +352 -0
  143. package/dist/developer/tutorial/testing.md +558 -0
  144. package/dist/developer/upgrades/2.0-to-2.1.md +46 -0
  145. package/dist/developer/upgrades/2.1-to-2.2.md +59 -0
  146. package/dist/developer/upgrades/2.2-to-2.3.md +44 -0
  147. package/dist/developer/upgrades/2.3-to-2.4.md +42 -0
  148. package/dist/developer/upgrades/3.0-to-3.1.md +47 -0
  149. package/dist/developer/upgrades/3.1-to-3.2.md +34 -0
  150. package/dist/developer/upgrades/3.2-to-3.3.md +70 -0
  151. package/dist/developer/upgrades/3.3-to-3.4.md +36 -0
  152. package/dist/developer/upgrades/3.4-to-3.5.md +44 -0
  153. package/dist/developer/upgrades/3.5-to-3.6.md +40 -0
  154. package/dist/developer/upgrades/3.6-to-3.7.md +62 -0
  155. package/dist/developer/upgrades/3.7-to-4.0.md +152 -0
  156. package/dist/developer/upgrades/4.0-to-4.1.md +92 -0
  157. package/dist/developer/upgrades/4.1-to-4.2.md +109 -0
  158. package/dist/developer/upgrades/4.10-to-5.0.md +129 -0
  159. package/dist/developer/upgrades/4.2-to-4.3.md +100 -0
  160. package/dist/developer/upgrades/4.3-to-4.4.md +125 -0
  161. package/dist/developer/upgrades/4.4-to-4.5.md +94 -0
  162. package/dist/developer/upgrades/4.5-to-4.6.md +119 -0
  163. package/dist/developer/upgrades/4.6-to-4.7.md +39 -0
  164. package/dist/developer/upgrades/4.8-to-4.9.md +24 -0
  165. package/dist/developer/upgrades/4.9-to-4.10.md +24 -0
  166. package/dist/developer/upgrades/4.x-to-4.8.md +52 -0
  167. package/dist/developer/upgrades/5.0-to-5.1.md +28 -0
  168. package/dist/developer/upgrades/5.1-to-5.2.md +127 -0
  169. package/dist/developer/upgrades/5.2-to-5.3.md +338 -0
  170. package/dist/developer/upgrades/5.3-to-5.4.md +248 -0
  171. package/dist/developer/upgrades/quickstart.md +36 -0
  172. package/dist/integrations/analytics/google-analytics.md +64 -0
  173. package/dist/integrations/analytics/google-tag-manager.md +78 -0
  174. package/dist/integrations/integrations.md +39 -0
  175. package/dist/integrations/marketing/klaviyo.md +99 -0
  176. package/dist/integrations/payments/adyen.md +90 -0
  177. package/dist/integrations/payments/paypal.md +41 -0
  178. package/dist/integrations/payments/razorpay.md +45 -0
  179. package/dist/integrations/payments/stripe.md +109 -0
  180. package/dist/integrations/search/meilisearch.md +236 -0
  181. package/dist/integrations/sso-mfa-social-login/admin-dashboard.md +57 -0
  182. package/dist/integrations/sso-mfa-social-login/storefront.md +56 -0
  183. 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