@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,387 @@
1
+ ---
2
+ title: Build a Custom Report
3
+ description: Step-by-step guide to creating custom reports with data queries, line item formatters, and CSV export.
4
+ ---
5
+
6
+ ## Overview
7
+
8
+ Spree's reporting system is designed for extension. Each report is a pair of classes — a **Report** that defines the data query and a **ReportLineItem** that formats each row — registered in `Spree.reports` so it appears in the admin UI.
9
+
10
+ This guide walks you through building a custom report from scratch, including advanced patterns for SQL aggregations and multi-vendor support.
11
+
12
+ Before starting, make sure you understand [how the reporting system works](/developer/core-concepts/reports).
13
+
14
+ ## Creating a Custom Report
15
+
16
+ ### Step 1: Create the Report Class
17
+
18
+ Create a new report class inheriting from `Spree::Report`. The key method to implement is `line_items_scope`, which returns an `ActiveRecord::Relation` defining the records in your report:
19
+
20
+ ```ruby app/models/spree/reports/customer_orders.rb
21
+ module Spree
22
+ module Reports
23
+ class CustomerOrders < Spree::Report
24
+ # Define the scope of records to include in the report
25
+ def line_items_scope
26
+ store.orders.complete.where(
27
+ completed_at: date_from..date_to,
28
+ currency: currency
29
+ ).includes(:user, :bill_address)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ ```
35
+
36
+ The `line_items_scope` method has access to:
37
+
38
+ | Helper | Description |
39
+ |--------|-------------|
40
+ | `store` | The current store |
41
+ | `date_from` | Report start date |
42
+ | `date_to` | Report end date |
43
+ | `currency` | Report currency |
44
+ | `vendor` | Vendor (if multi-vendor is enabled) |
45
+
46
+ ### Step 2: Create the Line Item Class
47
+
48
+ Create a corresponding line item class that transforms each record into report columns. The class name must match the report class name (e.g., `Reports::CustomerOrders` → `ReportLineItems::CustomerOrders`):
49
+
50
+ ```ruby app/models/spree/report_line_items/customer_orders.rb
51
+ module Spree
52
+ module ReportLineItems
53
+ class CustomerOrders < Spree::ReportLineItem
54
+ # Define attributes that will become columns
55
+ attribute :order_number, :string
56
+ attribute :completed_at, :string
57
+ attribute :customer_email, :string
58
+ attribute :customer_name, :string
59
+ attribute :item_count, :integer
60
+ attribute :total, :string
61
+
62
+ # Map record fields to report columns
63
+ def order_number
64
+ record.number
65
+ end
66
+
67
+ def completed_at
68
+ record.completed_at.strftime('%Y-%m-%d %H:%M')
69
+ end
70
+
71
+ def customer_email
72
+ record.email
73
+ end
74
+
75
+ def customer_name
76
+ record.bill_address&.full_name
77
+ end
78
+
79
+ def item_count
80
+ record.line_items.sum(:quantity)
81
+ end
82
+
83
+ def total
84
+ Spree::Money.new(record.total, currency: currency)
85
+ end
86
+ end
87
+ end
88
+ end
89
+ ```
90
+
91
+ **Important notes:**
92
+ - Use `attribute` to define columns with their types — these become CSV headers
93
+ - Each attribute needs a corresponding method that extracts/formats data from `record`
94
+ - `record` is a single item from `line_items_scope`
95
+ - Use `Spree::Money` for currency formatting
96
+ - `currency` and `store` are delegated from the report
97
+
98
+ #### Available Base Class Methods
99
+
100
+ `Spree::ReportLineItem` provides:
101
+
102
+ ```ruby
103
+ # Returns column headers for display
104
+ self.headers
105
+ # => [{ name: :order_number, label: "Order Number" }, ...]
106
+
107
+ # Returns column names for CSV header row
108
+ self.csv_headers
109
+ # => ["order_number", "completed_at", "customer_email", ...]
110
+
111
+ # Converts line item to CSV row array
112
+ to_csv
113
+ # => ["R123456", "2025-01-15 14:30", "john@example.com", ...]
114
+ ```
115
+
116
+ ### Step 3: Register the Report
117
+
118
+ Add your report to the registry in an initializer:
119
+
120
+ ```ruby config/initializers/spree.rb
121
+ Rails.application.config.after_initialize do
122
+ Spree.reports << Spree::Reports::CustomerOrders
123
+ end
124
+ ```
125
+
126
+ ### Step 4: Add Translations
127
+
128
+ Add the report name and column header translations:
129
+
130
+ ```yaml config/locales/en.yml
131
+ en:
132
+ spree:
133
+ report_names:
134
+ customer_orders: Customer Orders
135
+ order_number: Order Number
136
+ completed_at: Completed At
137
+ customer_email: Customer Email
138
+ customer_name: Customer Name
139
+ item_count: Item Count
140
+ total: Total
141
+ ```
142
+
143
+ After restarting your application, the new report will be available in **Admin > Reports**.
144
+
145
+ ## Advanced Patterns
146
+
147
+ ### Complex Queries with Aggregations
148
+
149
+ For reports that aggregate data across records, use SQL directly in `line_items_scope`:
150
+
151
+ ```ruby app/models/spree/reports/revenue_by_category.rb
152
+ module Spree
153
+ module Reports
154
+ class RevenueByCategory < Spree::Report
155
+ def line_items_scope
156
+ line_items_sql = Spree::LineItem
157
+ .joins(:order)
158
+ .where(
159
+ spree_orders: {
160
+ completed_at: date_from..date_to,
161
+ currency: currency
162
+ }
163
+ )
164
+ .select(
165
+ "spree_line_items.variant_id",
166
+ "SUM(spree_line_items.quantity) as quantity",
167
+ "SUM(spree_line_items.pre_tax_amount) as revenue"
168
+ )
169
+ .group(:variant_id)
170
+ .to_sql
171
+
172
+ store.taxons
173
+ .where(depth: 1) # Top-level categories
174
+ .joins("LEFT JOIN spree_products_taxons ON spree_products_taxons.taxon_id = spree_taxons.id")
175
+ .joins("LEFT JOIN spree_variants ON spree_variants.product_id = spree_products_taxons.product_id")
176
+ .joins("LEFT JOIN (#{line_items_sql}) AS line_items ON line_items.variant_id = spree_variants.id")
177
+ .select(
178
+ "spree_taxons.*",
179
+ "COALESCE(SUM(line_items.quantity), 0) AS total_quantity",
180
+ "COALESCE(SUM(line_items.revenue), 0.0) AS total_revenue"
181
+ )
182
+ .group("spree_taxons.id")
183
+ end
184
+ end
185
+ end
186
+ end
187
+ ```
188
+
189
+ When using aggregated queries, the `record` in your line item class will have virtual attributes (like `total_quantity`, `total_revenue`) available as methods.
190
+
191
+ ### Custom Summary Section
192
+
193
+ Override `summary` to provide aggregate metrics alongside the line items:
194
+
195
+ ```ruby app/models/spree/reports/customer_orders.rb
196
+ module Spree
197
+ module Reports
198
+ class CustomerOrders < Spree::Report
199
+ def summary
200
+ {
201
+ total_orders: line_items_scope.count,
202
+ total_revenue: line_items_scope.sum(:total),
203
+ average_order_value: line_items_scope.average(:total)
204
+ }
205
+ end
206
+ end
207
+ end
208
+ end
209
+ ```
210
+
211
+ ### Multi-Vendor Support
212
+
213
+ If you're using Spree Multi-Vendor, filter by vendor when one is selected:
214
+
215
+ ```ruby app/models/spree/reports/vendor_sales.rb
216
+ module Spree
217
+ module Reports
218
+ class VendorSales < Spree::Report
219
+ def line_items_scope
220
+ scope = store.line_items.where(
221
+ order: Spree::Order.complete.where(
222
+ completed_at: date_from..date_to,
223
+ currency: currency
224
+ )
225
+ )
226
+
227
+ # Filter by vendor if one is selected
228
+ scope = scope.where(vendor_id: vendor.id) if defined?(vendor) && vendor.present?
229
+
230
+ scope
231
+ end
232
+ end
233
+ end
234
+ end
235
+ ```
236
+
237
+ ## Testing Custom Reports
238
+
239
+ ### Testing the Report Class
240
+
241
+ Test that your report's `line_items_scope` returns the correct records:
242
+
243
+ ```ruby spec/models/spree/reports/customer_orders_spec.rb
244
+ require 'rails_helper'
245
+
246
+ RSpec.describe Spree::Reports::CustomerOrders, type: :model do
247
+ let(:store) { create(:store) }
248
+ let(:user) { create(:admin_user) }
249
+
250
+ subject(:report) do
251
+ described_class.new(
252
+ store: store,
253
+ user: user,
254
+ currency: 'USD',
255
+ date_from: 1.month.ago,
256
+ date_to: Time.current
257
+ )
258
+ end
259
+
260
+ describe '#line_items_scope' do
261
+ let!(:completed_order) do
262
+ create(:completed_order_with_totals, store: store, currency: 'USD', completed_at: 1.week.ago)
263
+ end
264
+
265
+ let!(:incomplete_order) do
266
+ create(:order, store: store, currency: 'USD', state: 'cart')
267
+ end
268
+
269
+ let!(:other_currency_order) do
270
+ create(:completed_order_with_totals, store: store, currency: 'EUR', completed_at: 1.week.ago)
271
+ end
272
+
273
+ let!(:old_order) do
274
+ create(:completed_order_with_totals, store: store, currency: 'USD', completed_at: 2.months.ago)
275
+ end
276
+
277
+ it 'returns only completed orders within the date range and currency' do
278
+ scope = report.line_items_scope
279
+
280
+ expect(scope).to include(completed_order)
281
+ expect(scope).not_to include(incomplete_order)
282
+ expect(scope).not_to include(other_currency_order)
283
+ expect(scope).not_to include(old_order)
284
+ end
285
+ end
286
+
287
+ describe '#line_items' do
288
+ let!(:order) do
289
+ create(:completed_order_with_totals, store: store, currency: 'USD', completed_at: 1.week.ago)
290
+ end
291
+
292
+ it 'returns ReportLineItem objects' do
293
+ items = report.line_items
294
+ expect(items).to all(be_a(Spree::ReportLineItems::CustomerOrders))
295
+ end
296
+
297
+ it 'respects the limit option' do
298
+ create(:completed_order_with_totals, store: store, currency: 'USD', completed_at: 2.days.ago)
299
+ items = report.line_items(limit: 1)
300
+ expect(items.length).to eq(1)
301
+ end
302
+ end
303
+ end
304
+ ```
305
+
306
+ ### Testing the ReportLineItem Class
307
+
308
+ Test that your line item correctly formats each record:
309
+
310
+ ```ruby spec/models/spree/report_line_items/customer_orders_spec.rb
311
+ require 'rails_helper'
312
+
313
+ RSpec.describe Spree::ReportLineItems::CustomerOrders, type: :model do
314
+ let(:store) { create(:store) }
315
+ let(:user) { create(:admin_user) }
316
+ let(:bill_address) { create(:address) }
317
+ let(:order) do
318
+ create(:completed_order_with_totals,
319
+ store: store,
320
+ currency: 'USD',
321
+ completed_at: 1.week.ago,
322
+ bill_address: bill_address)
323
+ end
324
+
325
+ let(:report) do
326
+ Spree::Reports::CustomerOrders.new(
327
+ store: store,
328
+ user: user,
329
+ currency: 'USD',
330
+ date_from: 1.month.ago,
331
+ date_to: Time.current
332
+ )
333
+ end
334
+
335
+ subject(:line_item) { described_class.new(record: order, report: report) }
336
+
337
+ describe '#order_number' do
338
+ it 'returns the order number' do
339
+ expect(line_item.order_number).to eq(order.number)
340
+ end
341
+ end
342
+
343
+ describe '#customer_name' do
344
+ it 'returns the billing address full name' do
345
+ expect(line_item.customer_name).to eq(bill_address.full_name)
346
+ end
347
+
348
+ context 'when no billing address' do
349
+ let(:bill_address) { nil }
350
+
351
+ it 'returns nil' do
352
+ order.update_column(:bill_address_id, nil)
353
+ order.reload
354
+ expect(line_item.customer_name).to be_nil
355
+ end
356
+ end
357
+ end
358
+
359
+ describe '#total' do
360
+ it 'returns a Spree::Money formatted total' do
361
+ result = line_item.total
362
+ expect(result).to be_a(Spree::Money)
363
+ expect(result.money.to_f).to eq(order.total.to_f)
364
+ end
365
+ end
366
+
367
+ describe '#to_csv' do
368
+ it 'returns an array of values for CSV row' do
369
+ csv_row = line_item.to_csv
370
+ expect(csv_row.length).to eq(6)
371
+ expect(csv_row[0]).to eq(order.number)
372
+ end
373
+ end
374
+ end
375
+ ```
376
+
377
+ ### Key Testing Patterns
378
+
379
+ 1. **Test scope filtering** — verify `line_items_scope` returns only records matching date range, currency, and store
380
+ 2. **Test attribute formatting** — verify each attribute method returns correctly formatted data
381
+ 3. **Test CSV output** — check `headers`, `csv_headers`, and `to_csv` return expected values
382
+ 4. **Test edge cases** — handle nil values gracefully (e.g., missing addresses)
383
+
384
+ ## Related Documentation
385
+
386
+ - [Reports](/developer/core-concepts/reports) - Report architecture and built-in reports
387
+ - [Events](/developer/core-concepts/events) - How report generation uses the events system
@@ -0,0 +1,230 @@
1
+ ---
2
+ title: Build a Custom Search Provider
3
+ description: Step-by-step guide to building a custom search provider for Spree, integrating external search engines like Typesense, Algolia, or Elasticsearch.
4
+ ---
5
+
6
+ ## Overview
7
+
8
+ This guide walks you through building a custom search provider for Spree. By the end, you'll have a fully functional search integration that:
9
+
10
+ - Powers product search, filtering, sorting, and faceted navigation
11
+ - Handles multi-locale and multi-currency indexing automatically
12
+ - Integrates with the Store API without any frontend changes
13
+ - Supports background indexing and bulk reindex
14
+
15
+ Before starting, make sure you understand [how search and filtering works in Spree](/developer/core-concepts/search-filtering).
16
+
17
+ > **INFO:** Spree ships with a built-in [Meilisearch provider](/integrations/search/meilisearch). If Meilisearch fits your needs, you don't need to build a custom provider — just configure it.
18
+
19
+ ## Architecture
20
+
21
+ ```
22
+ Store API Request (locale=de, currency=EUR)
23
+
24
+ ├─ AR Scope (security + visibility)
25
+ │ store.products.active(currency).accessible_by(ability)
26
+
27
+ └─ Search Provider (search + filter + facets)
28
+ ├─ Database (default): ILIKE + Ransack + FiltersAggregator
29
+ ├─ Meilisearch (built-in): one API call with locale/currency filtering
30
+ └─ Your Provider: implements the same interface
31
+ ```
32
+
33
+ The controller builds a base ActiveRecord scope for **security and visibility**, then delegates **everything else** to your search provider.
34
+
35
+ ## Step 1: Create the Provider Class
36
+
37
+ Create a class that inherits from `Spree::SearchProvider::Base` and implements `search_and_filter`:
38
+
39
+ ```ruby app/models/my_app/search_provider/typesense.rb
40
+ module MyApp
41
+ module SearchProvider
42
+ class Typesense < Spree::SearchProvider::Base
43
+ # Enable background indexing jobs
44
+ def self.indexing_required?
45
+ true
46
+ end
47
+
48
+ def initialize(store)
49
+ super
50
+ require 'typesense'
51
+ rescue LoadError
52
+ raise LoadError, "Add `gem 'typesense'` to your Gemfile"
53
+ end
54
+ end
55
+ end
56
+ end
57
+ ```
58
+
59
+ `indexing_required?` returning `true` tells the `SearchIndexable` concern to enqueue background jobs when products are created, updated, or destroyed.
60
+
61
+ ## Step 2: Implement `search_and_filter`
62
+
63
+ This is the core method. It receives a base AR scope (already filtered for security) and must return a `SearchResult`:
64
+
65
+ ```ruby app/models/my_app/search_provider/typesense.rb
66
+ def search_and_filter(scope:, query: nil, filters: {}, sort: nil, page: 1, limit: 25)
67
+ page = [page.to_i, 1].max
68
+ limit = limit.to_i.clamp(1, 100)
69
+
70
+ # 1. Query your search engine with locale/currency filtering
71
+ results = client.collections[index_name].documents.search({
72
+ q: query || '*',
73
+ query_by: 'name,description,sku,option_values,category_names,tags',
74
+ filter_by: build_filters(filters),
75
+ sort_by: build_sort(sort),
76
+ page: page,
77
+ per_page: limit,
78
+ facet_by: 'in_stock,price,category_ids,option_value_ids'
79
+ })
80
+
81
+ # 2. Extract product IDs (documents have composite IDs, extract product_id)
82
+ product_ids = results['hits'].map { |h| h['document']['product_id'] }.uniq
83
+ raw_ids = product_ids.filter_map { |pid| Spree::Product.decode_prefixed_id(pid) }
84
+
85
+ # 3. Intersect with AR scope (safety net for authorization)
86
+ products = raw_ids.any? ? scope.where(id: raw_ids).reorder(nil) : scope.none
87
+
88
+ # 4. Build Pagy for pagination metadata
89
+ require 'pagy'
90
+ pagy = Pagy::Offset.new(count: results['found'], page: page, limit: limit)
91
+
92
+ # 5. Return a SearchResult
93
+ Spree::SearchProvider::SearchResult.new(
94
+ products: products,
95
+ filters: build_facet_response(results['facet_counts']),
96
+ sort_options: %w[price -price name -name best_selling -available_on].map { |id| { id: id } },
97
+ default_sort: 'manual',
98
+ total_count: results['found'],
99
+ pagy: pagy
100
+ )
101
+ end
102
+ ```
103
+
104
+ > **WARNING:** Always filter by `locale`, `currency`, `store_ids`, `status='active'`, and `discontinue_on` in your search engine — not just in the AR scope. This ensures pagination counts are accurate. The AR scope is a safety net, not the primary filter.
105
+
106
+ ## Step 3: Implement Indexing
107
+
108
+ Products are indexed as **one document per market × locale** combination. The `ProductPresenter` handles this automatically:
109
+
110
+ ```ruby app/models/my_app/search_provider/typesense.rb
111
+ def index(product)
112
+ documents = Spree::SearchProvider::ProductPresenter.new(product, store).call
113
+ documents.each do |doc|
114
+ client.collections[index_name].documents.upsert(doc)
115
+ end
116
+ end
117
+
118
+ def remove(product)
119
+ remove_by_id(product.prefixed_id)
120
+ end
121
+
122
+ def remove_by_id(prefixed_id)
123
+ # Delete all locale/currency variants of this product
124
+ client.collections[index_name].documents.delete(
125
+ filter_by: "product_id:=#{prefixed_id}"
126
+ )
127
+ end
128
+ ```
129
+
130
+ The `ProductPresenter` returns an array of documents. For a store with US (USD/English) and EU (EUR/German+French) markets, one product produces 3 documents — each with flat `name`, `price`, `locale`, `currency` fields.
131
+
132
+ ## Step 4: Implement Bulk Reindex
133
+
134
+ Use `ProductPresenter::REQUIRED_PRELOADS` to avoid N+1 queries:
135
+
136
+ ```ruby app/models/my_app/search_provider/typesense.rb
137
+ def reindex(scope = nil)
138
+ scope ||= store.products
139
+ ensure_index_settings!
140
+
141
+ scope.reorder(id: :asc)
142
+ .preload(*Spree::SearchProvider::ProductPresenter::REQUIRED_PRELOADS)
143
+ .find_in_batches(batch_size: 500) do |batch|
144
+ documents = batch.flat_map { |p| Spree::SearchProvider::ProductPresenter.new(p, store).call }
145
+ index_batch(documents)
146
+ end
147
+ end
148
+
149
+ def index_batch(documents)
150
+ client.collections[index_name].documents.import(documents, action: 'upsert')
151
+ end
152
+
153
+ def ensure_index_settings!
154
+ # Create/update your collection schema here
155
+ end
156
+ ```
157
+
158
+ > **INFO:** Use `flat_map` (not `map`) because `ProductPresenter#call` returns an array of documents per product.
159
+
160
+ ## Step 5: Register the Provider
161
+
162
+ ```ruby config/initializers/spree.rb
163
+ Spree.search_provider = 'MyApp::SearchProvider::Typesense'
164
+ ```
165
+
166
+ Then reindex:
167
+
168
+ ```bash
169
+ rake spree:search:reindex
170
+ ```
171
+
172
+ ## Provider Contract Reference
173
+
174
+ | Method | Required | Description |
175
+ |--------|----------|-------------|
176
+ | `search_and_filter(scope:, query:, filters:, sort:, page:, limit:)` | Yes | Search, filter, sort, paginate, and return facets. Must return a `SearchResult`. |
177
+ | `self.indexing_required?` | Yes | Return `true` to enable background indexing jobs. |
178
+ | `index(product)` | Yes | Index a single product. `ProductPresenter#call` returns an array of documents. |
179
+ | `remove(product)` | Yes | Remove all locale/currency variants from the index. |
180
+ | `remove_by_id(prefixed_id)` | Yes | Remove by prefixed product ID (product may already be deleted). |
181
+ | `reindex(scope)` | Yes | Bulk reindex with `ensure_index_settings!` + batch indexing. |
182
+ | `index_batch(documents)` | Yes | Index a batch of pre-serialized documents. |
183
+ | `ensure_index_settings!` | No | Configure index schema. Called by `reindex` and rake task. |
184
+
185
+ ### SearchResult
186
+
187
+ ```ruby
188
+ Spree::SearchProvider::SearchResult.new(
189
+ products: ar_relation, # ActiveRecord::Relation
190
+ filters: [...], # Array of facet hashes
191
+ sort_options: [{ id: 'price' }, ...], # Array of sort option objects
192
+ default_sort: 'manual', # Default sort string
193
+ total_count: 150, # Total before pagination
194
+ pagy: pagy_object # Pagy::Offset or Pagy::Meilisearch
195
+ )
196
+ ```
197
+
198
+ ### ProductPresenter
199
+
200
+ ```ruby
201
+ documents = Spree::SearchProvider::ProductPresenter.new(product, store).call
202
+ # => [
203
+ # { prefixed_id: "prod_abc_en_USD", product_id: "prod_abc", locale: "en",
204
+ # currency: "USD", name: "Blue Shirt", price: 29.99, ... },
205
+ # { prefixed_id: "prod_abc_de_EUR", product_id: "prod_abc", locale: "de",
206
+ # currency: "EUR", name: "Blaues Hemd", price: 27.50, ... }
207
+ # ]
208
+ ```
209
+
210
+ ### Indexing Lifecycle
211
+
212
+ The `Spree::SearchIndexable` concern on Product provides:
213
+
214
+ | Method | Description |
215
+ |--------|-------------|
216
+ | `product.add_to_search_index` | Index synchronously (inline) |
217
+ | `product.remove_from_search_index` | Remove synchronously (inline) |
218
+ | `product.search_presentation` | Preview the documents that would be indexed |
219
+ | `rake spree:search:reindex` | Bulk reindex all products |
220
+
221
+ Background jobs (`IndexJob`, `RemoveJob`) fire on `after_commit` when `indexing_required?` is `true`.
222
+
223
+ ### Important: Prefixed IDs
224
+
225
+ Always use prefixed IDs (`ctg_abc`, `prod_xyz`, `optval_abc`) when indexing. Never use raw database IDs — Spree supports UUID primary keys.
226
+
227
+ ## Related Documentation
228
+
229
+ - [Search & Filtering](/developer/core-concepts/search-filtering) — Store API search reference
230
+ - [Meilisearch Integration](/integrations/search/meilisearch) — Built-in Meilisearch provider setup
@@ -0,0 +1,71 @@
1
+ ---
2
+ title: Multi-Store Spree Commerce
3
+ sidebarTitle: Quickstart
4
+ ---
5
+
6
+ To enable multiple stores, you will need to add the `spree_multi_store` gem to your project via:
7
+
8
+ ```bash
9
+ bundle add spree_multi_store
10
+ ```
11
+
12
+ > **INFO:** Spree Multi-Store is licensed under the [AGPL v3 License](https://opensource.org/licenses/AGPL-3.0).
13
+ >
14
+ > If you would like to use it in a commercial application, please [contact us](https://spreecommerce.org/get-started/) to obtain a commercial license.
15
+
16
+ ![](/images/mulit_store_978x2.png)
17
+
18
+ ## Store resources
19
+
20
+ Each Store can have its own resources. For example, a Store can have its own Products, Taxonomies, Promotions, etc.
21
+
22
+ | Resource | Relationship |
23
+ | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
24
+ | [**Order**](/core-concepts/orders) | One Order belongs to one Store |
25
+ | [**Product**](/core-concepts/products) | One Product can be associated with many Store(s), you can pick and choose in which Store(s) each Product will be available |
26
+ | [**Payment Method**](/core-concepts/payments) | One Payment Method can be associated with many Store(s), you can select in which Stores given Payment Method will be available on Checkout |
27
+ | [**Taxonomy**](/core-concepts/products#taxons-and-taxonomies) | One Taxonomy belongs to one Store |
28
+ | [**Promotion**](/core-concepts/promotions) | One Promotion can be associated with multiple Stores |
29
+ | **Store Credit** | One Store Credit belongs to and can be used in one Store
30
+
31
+ ## Current Store
32
+
33
+ Spree will try to determine the current store based on the current URL. If the URL does not match any of the stores in the database, Spree will fall back to the default store.
34
+
35
+ All Spree controllers or any other controllers that include [Spree::Core::ControllerHelpers::Store](https://github.com/spree/spree/blob/main/core/lib/spree/core/controller_helpers/store.rb) have access to the `current_store` method which returns the `Store` matching the current URL.
36
+
37
+ All parts of Spree (API, Admin Panel) have this implemented.
38
+
39
+ > **INFO:** Under the hood `current_store` calls [Spree::Stores::FindCurrent.new(url: url).execute](https://github.com/spree/spree/blob/main/core/app/finders/spree/stores/find_current.rb).
40
+ >
41
+ > This logic can be easily overwritten by setting
42
+ >
43
+ > ```ruby
44
+ Spree::Dependencies.current_store_finder = 'MyStoreFinderClass'
45
+ ```
46
+ >
47
+ > in `config/initializers/spree.rb` file
48
+
49
+ ## Default Store
50
+
51
+ If the system cannot find any Store that matches the current URL it will fall back to the Default Store.
52
+
53
+ You can set the default Store via Rails console:
54
+
55
+ ```ruby
56
+ Spree::Store.find(2).update(default: true)
57
+ ```
58
+
59
+ To get the default store in your code or rails console type:
60
+
61
+ ```ruby
62
+ Spree::Store.default
63
+ ```
64
+
65
+ ## Custom Domains
66
+
67
+ Spree supports managing custom domains for Stores. In the Admin Panel, you can manage custom domains for each Store in the **Settings -> Domains** page.
68
+
69
+ ## Setup multi-store application
70
+
71
+ [Follow the setup guide](/developer/multi-store/setup) to get started.