spree_core 5.2.6 → 5.3.0.rc1

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 (294) hide show
  1. checksums.yaml +4 -4
  2. data/app/helpers/spree/addresses_helper.rb +1 -23
  3. data/app/helpers/spree/base_helper.rb +25 -77
  4. data/app/helpers/spree/currency_helper.rb +2 -2
  5. data/app/helpers/spree/images_helper.rb +5 -0
  6. data/app/helpers/spree/mail_helper.rb +1 -1
  7. data/app/helpers/spree/products_helper.rb +1 -1
  8. data/app/jobs/spree/events/subscriber_job.rb +58 -0
  9. data/app/jobs/spree/products/refresh_metrics_job.rb +14 -0
  10. data/app/jobs/spree/products/touch_taxons_job.rb +0 -1
  11. data/app/jobs/spree/variants/touch_job.rb +9 -0
  12. data/app/models/concerns/spree/adjustment_source.rb +8 -0
  13. data/app/models/concerns/spree/parameterizable_name.rb +1 -1
  14. data/app/models/concerns/spree/product_scopes.rb +54 -8
  15. data/app/models/concerns/spree/publishable.rb +253 -0
  16. data/app/models/concerns/spree/unique_name.rb +1 -1
  17. data/app/models/concerns/spree/user_methods.rb +21 -1
  18. data/app/models/spree/ability.rb +3 -3
  19. data/app/models/spree/address.rb +0 -3
  20. data/app/models/spree/adjustable/promotion_accumulator.rb +1 -1
  21. data/app/models/spree/adjustment.rb +32 -6
  22. data/app/models/spree/asset.rb +6 -3
  23. data/app/models/spree/base.rb +3 -1
  24. data/app/models/spree/classification.rb +2 -2
  25. data/app/models/spree/credit_card.rb +0 -3
  26. data/app/models/spree/current.rb +24 -3
  27. data/app/models/spree/custom_domain.rb +1 -1
  28. data/app/models/spree/customer_group.rb +94 -0
  29. data/app/models/spree/customer_group_user.rb +16 -0
  30. data/app/models/spree/customer_return.rb +2 -3
  31. data/app/models/spree/digital.rb +2 -4
  32. data/app/models/spree/digital_link.rb +2 -3
  33. data/app/models/spree/event.rb +104 -0
  34. data/app/models/spree/export.rb +7 -1
  35. data/app/models/spree/gift_card.rb +13 -1
  36. data/app/models/spree/gift_card_batch.rb +3 -1
  37. data/app/models/spree/image.rb +20 -5
  38. data/app/models/spree/import.rb +17 -12
  39. data/app/models/spree/import_row.rb +13 -26
  40. data/app/models/spree/inventory_unit.rb +0 -3
  41. data/app/models/spree/invitation.rb +12 -6
  42. data/app/models/spree/line_item.rb +57 -4
  43. data/app/models/spree/newsletter_subscriber.rb +2 -0
  44. data/app/models/spree/option_type.rb +2 -5
  45. data/app/models/spree/option_value.rb +1 -4
  46. data/app/models/spree/order/checkout.rb +7 -2
  47. data/app/models/spree/order/emails.rb +3 -11
  48. data/app/models/spree/order/store_credit.rb +5 -1
  49. data/app/models/spree/order.rb +80 -31
  50. data/app/models/spree/order_updater.rb +28 -9
  51. data/app/models/spree/payment/custom_events.rb +37 -0
  52. data/app/models/spree/payment.rb +13 -4
  53. data/app/models/spree/payment_capture_event.rb +0 -4
  54. data/app/models/spree/payment_method.rb +1 -1
  55. data/app/models/spree/permission_sets/default_customer.rb +3 -3
  56. data/app/models/spree/policy.rb +1 -8
  57. data/app/models/spree/post.rb +2 -7
  58. data/app/models/spree/post_category.rb +2 -0
  59. data/app/models/spree/price.rb +8 -6
  60. data/app/models/spree/price_list.rb +263 -0
  61. data/app/models/spree/price_rule.rb +26 -0
  62. data/app/models/spree/price_rules/customer_group_rule.rb +21 -0
  63. data/app/models/spree/price_rules/user_rule.rb +19 -0
  64. data/app/models/spree/price_rules/volume_rule.rb +21 -0
  65. data/app/models/spree/price_rules/zone_rule.rb +19 -0
  66. data/app/models/spree/product.rb +117 -76
  67. data/app/models/spree/product_property.rb +2 -2
  68. data/app/models/spree/promotion/rules/customer_group.rb +22 -0
  69. data/app/models/spree/promotion/rules/user.rb +2 -2
  70. data/app/models/spree/promotion.rb +3 -4
  71. data/app/models/spree/promotion_handler/coupon.rb +3 -3
  72. data/app/models/spree/property.rb +3 -6
  73. data/app/models/spree/prototype.rb +0 -3
  74. data/app/models/spree/refund.rb +2 -3
  75. data/app/models/spree/reimbursement.rb +5 -3
  76. data/app/models/spree/report.rb +7 -1
  77. data/app/models/spree/reports/products_performance.rb +1 -1
  78. data/app/models/spree/reports/sales_total.rb +1 -1
  79. data/app/models/spree/return_authorization.rb +7 -3
  80. data/app/models/spree/return_item.rb +15 -4
  81. data/app/models/spree/shipment/custom_events.rb +47 -0
  82. data/app/models/spree/shipment.rb +12 -5
  83. data/app/models/spree/shipping_category.rb +0 -3
  84. data/app/models/spree/shipping_method.rb +0 -3
  85. data/app/models/spree/stock/quantifier.rb +1 -1
  86. data/app/models/spree/stock_item.rb +2 -0
  87. data/app/models/spree/stock_location.rb +0 -3
  88. data/app/models/spree/stock_movement/custom_events.rb +44 -0
  89. data/app/models/spree/stock_movement.rb +3 -0
  90. data/app/models/spree/stock_transfer.rb +2 -3
  91. data/app/models/spree/store.rb +64 -92
  92. data/app/models/spree/store_credit.rb +3 -4
  93. data/app/models/spree/store_product.rb +14 -0
  94. data/app/models/spree/subscriber.rb +186 -0
  95. data/app/models/spree/tax_category.rb +0 -4
  96. data/app/models/spree/tax_rate.rb +0 -3
  97. data/app/models/spree/taxon.rb +3 -36
  98. data/app/models/spree/taxonomy.rb +0 -3
  99. data/app/models/spree/variant.rb +114 -21
  100. data/app/models/spree/webhook_delivery.rb +60 -0
  101. data/app/models/spree/webhook_endpoint.rb +53 -0
  102. data/app/models/spree/wished_item.rb +2 -4
  103. data/app/models/spree/wishlist.rb +2 -3
  104. data/app/models/spree/zone.rb +0 -9
  105. data/app/paginators/spree/shared/paginate.rb +4 -0
  106. data/app/serializers/spree/events/asset_serializer.rb +22 -0
  107. data/app/serializers/spree/events/base_serializer.rb +61 -0
  108. data/app/serializers/spree/events/customer_return_serializer.rb +20 -0
  109. data/app/serializers/spree/events/digital_link_serializer.rb +20 -0
  110. data/app/serializers/spree/events/digital_serializer.rb +18 -0
  111. data/app/serializers/spree/events/export_serializer.rb +22 -0
  112. data/app/serializers/spree/events/gift_card_batch_serializer.rb +24 -0
  113. data/app/serializers/spree/events/gift_card_serializer.rb +29 -0
  114. data/app/serializers/spree/events/image_serializer.rb +9 -0
  115. data/app/serializers/spree/events/import_row_serializer.rb +23 -0
  116. data/app/serializers/spree/events/import_serializer.rb +24 -0
  117. data/app/serializers/spree/events/invitation_serializer.rb +28 -0
  118. data/app/serializers/spree/events/line_item_serializer.rb +31 -0
  119. data/app/serializers/spree/events/newsletter_subscriber_serializer.rb +21 -0
  120. data/app/serializers/spree/events/order_serializer.rb +39 -0
  121. data/app/serializers/spree/events/payment_serializer.rb +24 -0
  122. data/app/serializers/spree/events/post_category_serializer.rb +20 -0
  123. data/app/serializers/spree/events/post_serializer.rb +26 -0
  124. data/app/serializers/spree/events/price_serializer.rb +22 -0
  125. data/app/serializers/spree/events/product_serializer.rb +24 -0
  126. data/app/serializers/spree/events/promotion_serializer.rb +32 -0
  127. data/app/serializers/spree/events/refund_serializer.rb +23 -0
  128. data/app/serializers/spree/events/reimbursement_serializer.rb +22 -0
  129. data/app/serializers/spree/events/report_serializer.rb +23 -0
  130. data/app/serializers/spree/events/return_authorization_serializer.rb +22 -0
  131. data/app/serializers/spree/events/return_item_serializer.rb +27 -0
  132. data/app/serializers/spree/events/shipment_serializer.rb +24 -0
  133. data/app/serializers/spree/events/stock_item_serializer.rb +22 -0
  134. data/app/serializers/spree/events/stock_movement_serializer.rb +22 -0
  135. data/app/serializers/spree/events/stock_transfer_serializer.rb +22 -0
  136. data/app/serializers/spree/events/store_credit_serializer.rb +30 -0
  137. data/app/serializers/spree/events/user_serializer.rb +18 -0
  138. data/app/serializers/spree/events/variant_serializer.rb +34 -0
  139. data/app/serializers/spree/events/wished_item_serializer.rb +20 -0
  140. data/app/serializers/spree/events/wishlist_serializer.rb +22 -0
  141. data/app/services/spree/addresses/update.rb +1 -1
  142. data/app/services/spree/cart/add_item.rb +1 -1
  143. data/app/services/spree/coupon_codes/coupon_codes_handler.rb +2 -1
  144. data/app/services/spree/data_feeds/google/required_attributes.rb +4 -4
  145. data/app/services/spree/newsletter/verify.rb +5 -0
  146. data/app/services/spree/products/auto_match_taxons.rb +1 -1
  147. data/app/services/spree/seeds/all.rb +1 -1
  148. data/app/services/spree/taxons/add_products.rb +8 -4
  149. data/app/services/spree/taxons/regenerate_products.rb +8 -0
  150. data/app/services/spree/taxons/remove_products.rb +12 -7
  151. data/app/subscribers/spree/event_log_subscriber.rb +64 -0
  152. data/app/subscribers/spree/export_subscriber.rb +26 -0
  153. data/app/subscribers/spree/invitation_email_subscriber.rb +40 -0
  154. data/app/subscribers/spree/product_metrics_subscriber.rb +29 -0
  155. data/app/subscribers/spree/report_subscriber.rb +26 -0
  156. data/config/locales/en.yml +126 -0
  157. data/db/migrate/20251110120000_create_spree_price_lists.rb +22 -0
  158. data/db/migrate/20251110120001_create_spree_price_rules.rb +13 -0
  159. data/db/migrate/20251110120002_add_price_list_id_to_spree_prices.rb +6 -0
  160. data/db/migrate/20251110120003_add_price_list_id_to_spree_line_items.rb +5 -0
  161. data/db/migrate/20251201172118_fix_indexes_on_friendly_id_slugs.rb +8 -0
  162. data/db/migrate/20251214000001_create_spree_webhook_endpoints.rb +19 -0
  163. data/db/migrate/20251214000002_create_spree_webhook_deliveries.rb +23 -0
  164. data/db/migrate/20251222000000_add_performance_indexes_to_spree_adjustments.rb +25 -0
  165. data/db/migrate/20260112000000_fix_spree_prices_unique_indexes.rb +33 -0
  166. data/db/migrate/20260115120000_create_spree_customer_groups.rb +14 -0
  167. data/db/migrate/20260115120001_create_spree_customer_group_users.rb +14 -0
  168. data/db/migrate/20260117140831_remove_not_null_constraint_from_policy_name.rb +5 -0
  169. data/db/migrate/20260118120000_add_statistics_to_store_products.rb +11 -0
  170. data/db/migrate/20260119120000_add_counter_caches_to_spree_products.rb +9 -0
  171. data/db/migrate/20260119170000_add_counter_caches_to_spree_taxons.rb +9 -0
  172. data/db/migrate/20260120120000_add_image_count_to_spree_variants.rb +9 -0
  173. data/lib/generators/spree/dummy/dummy_generator.rb +14 -2
  174. data/lib/spree/core/configuration.rb +1 -0
  175. data/lib/spree/core/controller_helpers/auth.rb +0 -15
  176. data/lib/spree/core/controller_helpers/currency.rb +13 -9
  177. data/lib/spree/core/controller_helpers/search.rb +1 -1
  178. data/lib/spree/core/controller_helpers/store.rb +5 -1
  179. data/lib/spree/core/engine.rb +61 -78
  180. data/lib/spree/core/importer/order.rb +1 -1
  181. data/lib/spree/core/importer/product.rb +1 -1
  182. data/lib/spree/core/preferences/preferable.rb +14 -1
  183. data/lib/spree/core/pricing/context.rb +63 -0
  184. data/lib/spree/core/pricing/resolver.rb +129 -0
  185. data/lib/spree/core/search/base.rb +1 -1
  186. data/lib/spree/core/token_generator.rb +1 -1
  187. data/lib/spree/core/version.rb +1 -1
  188. data/lib/spree/core.rb +42 -47
  189. data/lib/spree/events/adapters/active_support_notifications.rb +112 -0
  190. data/lib/spree/events/adapters/base.rb +193 -0
  191. data/lib/spree/events/registry.rb +99 -0
  192. data/lib/spree/events.rb +240 -0
  193. data/lib/spree/permitted_attributes.rb +13 -2
  194. data/lib/spree/testing_support/common_rake.rb +68 -35
  195. data/lib/spree/testing_support/factories/customer_group_factory.rb +6 -0
  196. data/lib/spree/testing_support/factories/customer_group_user_factory.rb +6 -0
  197. data/lib/spree/testing_support/factories/price_factory.rb +4 -0
  198. data/lib/spree/testing_support/factories/price_list_factory.rb +34 -0
  199. data/lib/spree/testing_support/factories/price_rule_factory.rb +49 -0
  200. data/lib/spree/testing_support/factories/promotion_rule_factory.rb +12 -0
  201. data/lib/spree/testing_support/factories/stock_item_factory.rb +6 -4
  202. data/lib/spree/testing_support/factories/store_product_factory.rb +6 -0
  203. data/lib/spree/testing_support/factories/taxon_factory.rb +0 -1
  204. data/lib/spree/testing_support/factories/webhook_delivery_factory.rb +48 -0
  205. data/lib/spree/testing_support/factories/webhook_endpoint_factory.rb +22 -0
  206. data/lib/spree/testing_support/lifecycle_events.rb +38 -0
  207. data/lib/spree/testing_support/store.rb +4 -2
  208. data/lib/spree/webhooks.rb +22 -0
  209. data/lib/tasks/products.rake +40 -0
  210. data/lib/tasks/taxons.rake +19 -0
  211. data/lib/tasks/variants.rake +18 -0
  212. metadata +112 -114
  213. data/app/jobs/spree/themes/duplicate_components_job.rb +0 -59
  214. data/app/jobs/spree/themes/screenshot_job.rb +0 -81
  215. data/app/models/concerns/spree/has_page_links.rb +0 -53
  216. data/app/models/spree/page.rb +0 -188
  217. data/app/models/spree/page_block.rb +0 -73
  218. data/app/models/spree/page_blocks/buttons.rb +0 -29
  219. data/app/models/spree/page_blocks/heading.rb +0 -18
  220. data/app/models/spree/page_blocks/image.rb +0 -20
  221. data/app/models/spree/page_blocks/link.rb +0 -21
  222. data/app/models/spree/page_blocks/mega_nav.rb +0 -33
  223. data/app/models/spree/page_blocks/mega_nav_with_subcategories.rb +0 -32
  224. data/app/models/spree/page_blocks/metafields.rb +0 -18
  225. data/app/models/spree/page_blocks/nav.rb +0 -15
  226. data/app/models/spree/page_blocks/newsletter_form.rb +0 -18
  227. data/app/models/spree/page_blocks/products/brand.rb +0 -15
  228. data/app/models/spree/page_blocks/products/buy_buttons.rb +0 -24
  229. data/app/models/spree/page_blocks/products/description.rb +0 -18
  230. data/app/models/spree/page_blocks/products/price.rb +0 -18
  231. data/app/models/spree/page_blocks/products/quantity_selector.rb +0 -20
  232. data/app/models/spree/page_blocks/products/share.rb +0 -8
  233. data/app/models/spree/page_blocks/products/title.rb +0 -19
  234. data/app/models/spree/page_blocks/products/variant_picker.rb +0 -13
  235. data/app/models/spree/page_blocks/subheading.rb +0 -17
  236. data/app/models/spree/page_blocks/text.rb +0 -16
  237. data/app/models/spree/page_link.rb +0 -60
  238. data/app/models/spree/page_section.rb +0 -222
  239. data/app/models/spree/page_sections/announcement_bar.rb +0 -28
  240. data/app/models/spree/page_sections/breadcrumbs.rb +0 -12
  241. data/app/models/spree/page_sections/collection_banner.rb +0 -18
  242. data/app/models/spree/page_sections/custom_code.rb +0 -11
  243. data/app/models/spree/page_sections/featured_posts.rb +0 -45
  244. data/app/models/spree/page_sections/featured_product.rb +0 -50
  245. data/app/models/spree/page_sections/featured_taxon.rb +0 -90
  246. data/app/models/spree/page_sections/featured_taxons.rb +0 -45
  247. data/app/models/spree/page_sections/footer.rb +0 -101
  248. data/app/models/spree/page_sections/header.rb +0 -62
  249. data/app/models/spree/page_sections/image_banner.rb +0 -55
  250. data/app/models/spree/page_sections/image_with_text.rb +0 -65
  251. data/app/models/spree/page_sections/main_password_footer.rb +0 -18
  252. data/app/models/spree/page_sections/main_password_header.rb +0 -20
  253. data/app/models/spree/page_sections/newsletter.rb +0 -54
  254. data/app/models/spree/page_sections/page_title.rb +0 -19
  255. data/app/models/spree/page_sections/post_details.rb +0 -19
  256. data/app/models/spree/page_sections/post_grid.rb +0 -19
  257. data/app/models/spree/page_sections/product_details.rb +0 -53
  258. data/app/models/spree/page_sections/product_grid.rb +0 -13
  259. data/app/models/spree/page_sections/related_products.rb +0 -58
  260. data/app/models/spree/page_sections/rich_text.rb +0 -31
  261. data/app/models/spree/page_sections/taxon_banner.rb +0 -18
  262. data/app/models/spree/page_sections/taxon_grid.rb +0 -17
  263. data/app/models/spree/page_sections/video.rb +0 -107
  264. data/app/models/spree/pages/account.rb +0 -19
  265. data/app/models/spree/pages/cart.rb +0 -19
  266. data/app/models/spree/pages/checkout.rb +0 -15
  267. data/app/models/spree/pages/custom.rb +0 -38
  268. data/app/models/spree/pages/homepage.rb +0 -72
  269. data/app/models/spree/pages/login.rb +0 -19
  270. data/app/models/spree/pages/password.rb +0 -59
  271. data/app/models/spree/pages/post.rb +0 -27
  272. data/app/models/spree/pages/post_list.rb +0 -36
  273. data/app/models/spree/pages/product_details.rb +0 -30
  274. data/app/models/spree/pages/search_results.rb +0 -43
  275. data/app/models/spree/pages/shop_all.rb +0 -40
  276. data/app/models/spree/pages/taxon.rb +0 -29
  277. data/app/models/spree/pages/taxon_list.rb +0 -41
  278. data/app/models/spree/pages/wishlist.rb +0 -15
  279. data/app/models/spree/theme.rb +0 -233
  280. data/app/models/spree/themes/default.rb +0 -97
  281. data/app/services/spree/taxons/touch_featured_sections.rb +0 -21
  282. data/db/migrate/20250120094216_create_page_builder_models.rb +0 -78
  283. data/db/migrate/20250305121352_remove_page_builder_indices.rb +0 -11
  284. data/db/migrate/20250825175217_add_missing_page_builder_indexes.rb +0 -7
  285. data/db/migrate/20250913130044_add_page_links_counter_cache_to_spree_stores.rb +0 -10
  286. data/lib/generators/spree/dummy/templates/initializers/devise.rb +0 -3
  287. data/lib/generators/spree/install/install_generator.rb +0 -219
  288. data/lib/generators/spree/install/templates/config/initializers/spree.rb +0 -126
  289. data/lib/spree/core/webhooks.rb +0 -21
  290. data/lib/spree/testing_support/factories/page_block_factory.rb +0 -22
  291. data/lib/spree/testing_support/factories/page_factory.rb +0 -33
  292. data/lib/spree/testing_support/factories/page_link_factory.rb +0 -7
  293. data/lib/spree/testing_support/factories/page_section_factory.rb +0 -27
  294. data/lib/spree/testing_support/factories/theme_factory.rb +0 -14
@@ -0,0 +1,240 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'events/registry'
4
+ require_relative 'events/adapters/base'
5
+ require_relative 'events/adapters/active_support_notifications'
6
+
7
+ module Spree
8
+ # Main entry point for the Spree event system.
9
+ #
10
+ # This module provides a clean API for publishing events and subscribing
11
+ # to them. It abstracts the underlying implementation (ActiveSupport::Notifications)
12
+ # allowing for future changes without affecting subscriber code.
13
+ #
14
+ # @example Publishing an event
15
+ # Spree::Events.publish('order.completed', order.serializable_hash)
16
+ #
17
+ # @example Subscribing with a class
18
+ # Spree::Events.subscribe('order.completed', OrderCompletedHandler)
19
+ #
20
+ # @example Subscribing with a block
21
+ # Spree::Events.subscribe('order.completed') do |event|
22
+ # puts "Order completed: #{event.payload['number']}"
23
+ # end
24
+ #
25
+ # @example Pattern matching
26
+ # Spree::Events.subscribe('order.*', OrderAuditLogger) # All order events
27
+ # Spree::Events.subscribe('*', GlobalEventLogger) # All events
28
+ #
29
+ module Events
30
+ class << self
31
+ # Publish an event to all matching subscribers
32
+ #
33
+ # @param name [String] The event name (e.g., 'order.complete')
34
+ # @param payload [Hash] The event payload (should be serializable)
35
+ # @param metadata [Hash] Additional metadata for the event
36
+ # @return [Spree::Event] The published event
37
+ #
38
+ # @example
39
+ # Spree::Events.publish('product.created', product.serializable_hash)
40
+ # Spree::Events.publish('order.completed', order.serializable_hash, { user_id: current_user.id })
41
+ #
42
+ def publish(name, payload = {}, metadata = {})
43
+ adapter.publish(name, payload, metadata)
44
+ end
45
+
46
+ # Subscribe to an event pattern
47
+ #
48
+ # @param pattern [String] Event pattern (supports wildcards like 'order.*' or '*')
49
+ # @param subscriber [Class, Proc, nil] The subscriber class or callable
50
+ # @param options [Hash] Subscription options
51
+ # @option options [Boolean] :async (true) Whether to run async via ActiveJob
52
+ # @yield [event] Block to execute when event occurs (alternative to subscriber param)
53
+ # @yieldparam event [Spree::Event] The event object
54
+ # @return [void]
55
+ #
56
+ # @example With a subscriber class
57
+ # Spree::Events.subscribe('order.completed', SendConfirmationEmail)
58
+ #
59
+ # @example With a block
60
+ # Spree::Events.subscribe('order.completed') { |event| puts event.name }
61
+ #
62
+ # @example With pattern matching
63
+ # Spree::Events.subscribe('order.*', OrderAuditLogger)
64
+ #
65
+ # @example Synchronous execution
66
+ # Spree::Events.subscribe('order.completed', MyHandler, async: false)
67
+ #
68
+ def subscribe(pattern, subscriber = nil, options = {}, &block)
69
+ subscriber = block if block_given?
70
+ raise ArgumentError, 'Must provide a subscriber class, callable, or block' unless subscriber
71
+
72
+ adapter.subscribe(pattern, subscriber, options)
73
+ end
74
+
75
+ # Unsubscribe from an event pattern
76
+ #
77
+ # @param pattern [String] Event pattern
78
+ # @param subscriber [Class, Proc] The subscriber to remove
79
+ # @return [Boolean] true if removed, false if not found
80
+ def unsubscribe(pattern, subscriber)
81
+ adapter.unsubscribe(pattern, subscriber)
82
+ end
83
+
84
+ # Get the event registry
85
+ #
86
+ # @return [Spree::Events::Registry]
87
+ def registry
88
+ @registry ||= Registry.new
89
+ end
90
+
91
+ # Get the adapter instance
92
+ #
93
+ # The adapter class can be configured via Spree.events_adapter_class
94
+ # Default: Spree::Events::Adapters::ActiveSupportNotifications
95
+ #
96
+ # @return [Object] the configured adapter instance
97
+ def adapter
98
+ @adapter ||= Spree.events_adapter_class.new(registry)
99
+ end
100
+
101
+ # Activate the event system (called during Rails initialization)
102
+ # Also registers all subscribers from Spree.subscribers
103
+ # This method is idempotent - calling it multiple times has no effect
104
+ def activate!
105
+ return if registry.size > 0
106
+
107
+ register_subscribers!
108
+ adapter.activate!
109
+ end
110
+
111
+ # Reset the event system (useful for testing)
112
+ def reset!
113
+ adapter.deactivate! if @adapter
114
+ @registry = nil
115
+ @adapter = nil
116
+ end
117
+
118
+ # Register all subscribers from Spree.subscribers
119
+ #
120
+ # This is called automatically during Rails initialization.
121
+ # Can also be called in tests after reset! to re-register subscribers.
122
+ #
123
+ # In development, class objects in Spree.subscribers may become stale after
124
+ # code reload (Zeitwerk creates new class objects). We resolve the constant
125
+ # fresh from the class name to ensure we're using the reloaded class.
126
+ #
127
+ # @return [void]
128
+ def register_subscribers!
129
+ return unless defined?(Spree) && Spree.respond_to?(:subscribers)
130
+
131
+ Spree.subscribers&.each do |subscriber|
132
+ # Resolve the subscriber constant fresh to handle code reload in development
133
+ # The array may contain stale class objects after Zeitwerk reload
134
+ resolved_subscriber = resolve_subscriber(subscriber)
135
+ next unless resolved_subscriber
136
+
137
+ resolved_subscriber.subscription_patterns.each do |pattern|
138
+ subscribe(pattern, resolved_subscriber, resolved_subscriber.subscription_options)
139
+ end
140
+ end
141
+ end
142
+
143
+ # Resolve a subscriber to its current class object
144
+ #
145
+ # In development, Zeitwerk may have reloaded the class, creating a new
146
+ # class object while the old one is still referenced in Spree.subscribers.
147
+ # This method resolves the constant fresh to get the current class.
148
+ #
149
+ # @param subscriber [Class, String] The subscriber class or class name
150
+ # @return [Class, nil] The resolved class or nil if not found
151
+ def resolve_subscriber(subscriber)
152
+ return subscriber unless Rails.env.development? || Rails.env.test?
153
+
154
+ class_name = subscriber.is_a?(String) ? subscriber : subscriber.name
155
+ return nil unless class_name
156
+
157
+ class_name.constantize
158
+ rescue NameError => e
159
+ Rails.logger.warn "[Spree Events] Could not resolve subscriber #{class_name}: #{e.message}"
160
+ nil
161
+ end
162
+
163
+ # List all registered subscriber patterns
164
+ #
165
+ # @return [Array<String>]
166
+ def patterns
167
+ registry.patterns
168
+ end
169
+
170
+ # List all subscriptions
171
+ #
172
+ # @return [Array<Spree::Events::Registry::Subscription>]
173
+ def subscriptions
174
+ registry.all_subscriptions
175
+ end
176
+
177
+ # Check if events are enabled
178
+ #
179
+ # Events can be temporarily disabled using Spree::Events.disable { ... }
180
+ #
181
+ # @return [Boolean]
182
+ def enabled?
183
+ !RequestStore.store[:spree_events_disabled]
184
+ end
185
+
186
+ # Temporarily disable events within a block
187
+ #
188
+ # @yield Block during which events are disabled
189
+ # @return [Object] Return value of the block
190
+ #
191
+ # @example
192
+ # Spree::Events.disable do
193
+ # # Events published here won't trigger subscribers
194
+ # order.complete!
195
+ # end
196
+ #
197
+ def disable
198
+ previous = RequestStore.store[:spree_events_disabled]
199
+ RequestStore.store[:spree_events_disabled] = true
200
+ yield
201
+ ensure
202
+ RequestStore.store[:spree_events_disabled] = previous
203
+ end
204
+
205
+ # Globally disable events
206
+ # Useful for testing to disable events for the entire test suite
207
+ # @return [void]
208
+ def disable!
209
+ RequestStore.store[:spree_events_disabled] = true
210
+ end
211
+
212
+ # Temporarily enable events within a block
213
+ # Useful for testing when events are globally disabled
214
+ #
215
+ # @yield Block during which events are enabled
216
+ # @return [Object] Return value of the block
217
+ #
218
+ # @example
219
+ # Spree::Events.enable do
220
+ # # Events published here will trigger subscribers
221
+ # order.complete!
222
+ # end
223
+ #
224
+ def enable
225
+ previous = RequestStore.store[:spree_events_disabled]
226
+ RequestStore.store[:spree_events_disabled] = false
227
+ yield
228
+ ensure
229
+ RequestStore.store[:spree_events_disabled] = previous
230
+ end
231
+
232
+ # Globally enable events
233
+ # Useful for testing
234
+ # @return [void]
235
+ def enable!
236
+ RequestStore.store[:spree_events_disabled] = false
237
+ end
238
+ end
239
+ end
240
+ end
@@ -8,6 +8,7 @@ module Spree
8
8
  :cms_page_attributes,
9
9
  :cms_section_attributes,
10
10
  :custom_domain_attributes,
11
+ :customer_group_attributes,
11
12
  :customer_return_attributes,
12
13
  :digital_attributes,
13
14
  :digital_link_attributes,
@@ -36,6 +37,7 @@ module Spree
36
37
  :policy_attributes,
37
38
  :post_attributes,
38
39
  :post_category_attributes,
40
+ :price_list_attributes,
39
41
  :product_attributes,
40
42
  :promotion_attributes,
41
43
  :promotion_rule_attributes,
@@ -68,6 +70,7 @@ module Spree
68
70
  :theme_attributes,
69
71
  :user_attributes,
70
72
  :variant_attributes,
73
+ :webhook_endpoint_attributes,
71
74
  :wishlist_attributes,
72
75
  :wished_item_attributes,
73
76
  :zone_attributes
@@ -88,7 +91,8 @@ module Spree
88
91
 
89
92
  @@checkout_attributes = [
90
93
  :coupon_code, :email, :shipping_method_id, :special_instructions, :use_billing, :use_shipping,
91
- :user_id, :bill_address_id, :ship_address_id, :accept_marketing, :signup_for_an_account, :currency
94
+ :user_id, :bill_address_id, :ship_address_id, :accept_marketing, :signup_for_an_account, :currency,
95
+ :internal_note
92
96
  ]
93
97
 
94
98
  @@classification_attributes = [
@@ -101,6 +105,8 @@ module Spree
101
105
 
102
106
  @@custom_domain_attributes = [:url, :default]
103
107
 
108
+ @@customer_group_attributes = [:name, :description]
109
+
104
110
  @@customer_return_attributes = [:stock_location_id, {
105
111
  return_items_attributes: [:id, :inventory_unit_id, :return_authorization_id, :returned, :pre_tax_amount,
106
112
  :acceptance_status, :exchange_variant_id, :resellable]
@@ -162,6 +168,8 @@ module Spree
162
168
 
163
169
  @@post_category_attributes = [:title, :slug, :description]
164
170
 
171
+ @@price_list_attributes = [:name, :description, :match_policy, :starts_at, :ends_at, :status, :position]
172
+
165
173
  @@product_properties_attributes = [:property_name, :property_id, :value, :position, :_destroy]
166
174
 
167
175
  @@product_attributes = [
@@ -290,7 +298,8 @@ module Spree
290
298
 
291
299
  @@user_attributes = [:email, :bill_address_id, :ship_address_id, :password, :first_name, :last_name,
292
300
  :password_confirmation, :selected_locale, :avatar, :accepts_email_marketing, :phone,
293
- { public_metadata: {}, private_metadata: {}, tag_list: [] }]
301
+ :internal_note,
302
+ { public_metadata: {}, private_metadata: {}, tag_list: [], customer_group_ids: [] }]
294
303
 
295
304
  @@variant_attributes = [
296
305
  :name, :presentation, :cost_price, :discontinue_on, :lock_version,
@@ -308,6 +317,8 @@ module Spree
308
317
  }
309
318
  ]
310
319
 
320
+ @@webhook_endpoint_attributes = [:url, :secret, :active, subscriptions: []]
321
+
311
322
  @@wishlist_attributes = [:name, :is_default, :is_private]
312
323
 
313
324
  @@wished_item_attributes = [:variant_id, :quantity]
@@ -1,7 +1,3 @@
1
- unless defined?(Spree::InstallGenerator)
2
- require 'generators/spree/install/install_generator'
3
- end
4
-
5
1
  require 'generators/spree/dummy/dummy_generator'
6
2
  require 'generators/spree/dummy_model/dummy_model_generator'
7
3
 
@@ -9,56 +5,85 @@ desc 'Generates a dummy app for testing'
9
5
  namespace :common do
10
6
  task :test_app, :user_class do |_t, args|
11
7
  args.with_defaults(
8
+ authentication: 'dummy',
12
9
  user_class: 'Spree::LegacyUser',
13
- install_storefront: 'false',
14
- install_admin: 'false',
15
- authentication: 'dummy'
10
+ install_storefront: false,
11
+ install_admin: false,
12
+ javascript: false,
13
+ css: false
16
14
  )
17
15
  require ENV['LIB_NAME'].to_s
18
16
 
17
+ # Convert to booleans (supports both string and boolean values for backwards compatibility)
18
+ install_admin = args[:install_admin].to_b
19
+ install_storefront = args[:install_storefront].to_b
20
+ javascript_enabled = args[:javascript].to_b
21
+ css_enabled = args[:css].to_b
22
+
23
+ # Admin and Storefront require JavaScript and CSS (Tailwind) to function properly
24
+ javascript_enabled ||= install_admin || install_storefront
25
+ css_enabled ||= install_admin || install_storefront
26
+
27
+ puts args
28
+
19
29
  ENV['RAILS_ENV'] = 'test'
20
30
  Rails.env = 'test'
21
31
 
22
- api_only = ['spree/api', 'spree/core', 'spree/sample'].include?(ENV['LIB_NAME'])
23
- skip_javascript = api_only || ENV['LIB_NAME'] == 'spree/emails'
24
-
25
32
  dummy_app_args = [
26
33
  "--lib_name=#{ENV['LIB_NAME']}"
27
34
  ]
28
- if api_only
29
- dummy_app_args << '--api'
30
- elsif skip_javascript
31
- dummy_app_args << '--skip_javascript'
32
- end
35
+ # Use API mode only if no frontend components are needed
36
+ use_api_mode = !install_storefront && !install_admin && !javascript_enabled && !css_enabled
37
+ dummy_app_args << '--api' if use_api_mode
38
+ dummy_app_args << '--javascript' if javascript_enabled
39
+ dummy_app_args << '--css=tailwind' if css_enabled
40
+
41
+ puts dummy_app_args
42
+
33
43
  Spree::DummyGenerator.start dummy_app_args
34
44
 
35
- unless skip_javascript
36
- system('bundle add sprockets-rails') # we need this until we will remove bootstrap/popper_js gems
37
- system('bundle exec rails importmap:install turbo:install stimulus:install')
45
+ # Install JavaScript dependencies (importmap, turbo, stimulus) if JavaScript is enabled
46
+ # Rails includes the gems but doesn't run the installers automatically
47
+ if javascript_enabled
48
+ puts 'Installing JavaScript dependencies...'
49
+ system('yes | bundle exec rails importmap:install turbo:install stimulus:install')
38
50
  end
39
51
 
40
52
  # install devise if it's not the legacy user, useful for testing storefront
41
53
  if args[:authentication] == 'devise' && args[:user_class] != 'Spree::LegacyUser'
42
54
  system('bundle exec rails g devise:install --force --auto-accept')
43
55
  system("bundle exec rails g devise #{args[:user_class]} --force --auto-accept")
56
+ system("bundle exec rails g devise #{args[:admin_user_class]} --force --auto-accept") if args[:admin_user_class].present? && args[:admin_user_class] != args[:user_class]
44
57
  system('rm -rf spec') # we need to cleanup factories created by devise to avoid naming conflict
45
58
  end
46
59
 
47
- Spree::InstallGenerator.start [
48
- "--lib_name=#{ENV['LIB_NAME']}",
49
- '--auto-accept',
50
- '--migrate=false',
51
- '--seed=false',
52
- '--sample=false',
53
- "--install_storefront=#{args[:install_storefront]}",
54
- "--install_admin=#{args[:install_admin]}",
55
- "--user_class=#{args[:user_class]}",
56
- "--authentication=#{args[:authentication]}"
57
- ]
60
+ # Run core Spree install generator
61
+ puts 'Running Spree install generator...'
62
+ system("bundle exec rails g spree:install --force --auto-accept --migrate=false --seed=false --sample=false --user_class=#{args[:user_class]} --admin_user_class=#{args[:admin_user_class]} --authentication=#{args[:authentication]}")
58
63
 
59
- if !skip_javascript || ENV['LIB_NAME'] == 'spree/emails'
60
- puts 'Precompiling assets...'
61
- system('bundle exec rake assets:precompile > /dev/null 2>&1')
64
+ # Determine if we need to install admin/storefront
65
+ # Either via explicit flag or because we're testing that gem itself
66
+ needs_admin = install_admin || ENV['LIB_NAME'] == 'spree/admin'
67
+ needs_storefront = install_storefront || ENV['LIB_NAME'] == 'spree/storefront'
68
+
69
+ # Run admin install generator if requested or testing admin gem
70
+ if needs_admin
71
+ # Only run install if explicitly requested (not when testing admin gem itself)
72
+ if install_admin
73
+ puts 'Running Spree Admin install generator...'
74
+ system('bundle exec rails g spree:admin:install --force')
75
+ end
76
+ system('bundle exec rails g spree:admin:devise --force') if args[:authentication] == 'devise'
77
+ end
78
+
79
+ # Run storefront install generator if requested or testing storefront gem
80
+ if needs_storefront
81
+ # Only run install if explicitly requested (not when testing storefront gem itself)
82
+ if install_storefront
83
+ puts 'Running Spree Storefront install generator...'
84
+ system('bundle exec rails g spree:storefront:install --force --migrate=false')
85
+ end
86
+ system('bundle exec rails g spree:storefront:devise --force') if args[:authentication] == 'devise'
62
87
  end
63
88
 
64
89
  unless ENV['NO_MIGRATE']
@@ -74,13 +99,21 @@ namespace :common do
74
99
  puts 'Running extension installation generator...'
75
100
 
76
101
  if ENV['NO_MIGRATE']
77
- "#{ENV['LIB_NAME'].camelize}::Generators::InstallGenerator".constantize.start([])
102
+ "#{ENV['LIB_NAME'].camelize}::Generators::InstallGenerator".constantize.start(['--force'])
78
103
  else
79
- "#{ENV['LIB_NAME'].camelize}::Generators::InstallGenerator".constantize.start(['--auto-run-migrations'])
104
+ "#{ENV['LIB_NAME'].camelize}::Generators::InstallGenerator".constantize.start(['--force', '--auto-run-migrations'])
80
105
  end
81
- rescue LoadError
106
+ rescue LoadError => e
107
+ puts "Error loading generator: #{e.message}"
82
108
  puts 'Skipping installation no generator to run...'
83
109
  end
110
+
111
+ # Precompile assets after all generators have run
112
+ # This ensures CSS entry points (like Spree Admin's Tailwind CSS) are created first
113
+ if javascript_enabled || css_enabled
114
+ puts 'Precompiling assets...'
115
+ system('bundle exec rake assets:precompile')
116
+ end
84
117
  end
85
118
 
86
119
  task :db_setup do |_t|
@@ -0,0 +1,6 @@
1
+ FactoryBot.define do
2
+ factory :customer_group, class: Spree::CustomerGroup do
3
+ sequence(:name) { |n| "Customer Group #{n}" }
4
+ store { Spree::Store.default || create(:store) }
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ FactoryBot.define do
2
+ factory :customer_group_user, class: Spree::CustomerGroupUser do
3
+ customer_group
4
+ user { create(:user) }
5
+ end
6
+ end
@@ -3,5 +3,9 @@ FactoryBot.define do
3
3
  variant
4
4
  amount { 19.99 }
5
5
  currency { 'USD' }
6
+
7
+ factory :price_eur do
8
+ currency { 'EUR' }
9
+ end
6
10
  end
7
11
  end
@@ -0,0 +1,34 @@
1
+ FactoryBot.define do
2
+ factory :price_list, class: Spree::PriceList do
3
+ sequence(:name) { |n| "Price List #{n}" }
4
+ store { Spree::Store.default || create(:store) }
5
+ match_policy { 'all' }
6
+
7
+ trait :draft do
8
+ status { 'draft' }
9
+ end
10
+
11
+ trait :active do
12
+ status { 'active' }
13
+ end
14
+
15
+ trait :inactive do
16
+ status { 'inactive' }
17
+ end
18
+
19
+ trait :scheduled do
20
+ status { 'scheduled' }
21
+ starts_at { 1.hour.from_now }
22
+ ends_at { 1.week.from_now }
23
+ end
24
+
25
+ trait :with_date_range do
26
+ starts_at { 1.day.ago }
27
+ ends_at { 1.day.from_now }
28
+ end
29
+
30
+ trait :any_match do
31
+ match_policy { 'any' }
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,49 @@
1
+ FactoryBot.define do
2
+ factory :price_rule, class: Spree::PriceRule do
3
+ price_list
4
+
5
+ factory :zone_price_rule, class: Spree::PriceRules::ZoneRule do
6
+ after(:build) do |rule, evaluator|
7
+ rule.preferred_zone_ids = evaluator.zone_ids if evaluator.respond_to?(:zone_ids)
8
+ end
9
+
10
+ transient do
11
+ zone_ids { [] }
12
+ end
13
+ end
14
+
15
+ factory :volume_price_rule, class: Spree::PriceRules::VolumeRule do
16
+ after(:build) do |rule, evaluator|
17
+ rule.preferred_min_quantity = evaluator.min_quantity if evaluator.min_quantity.present?
18
+ rule.preferred_max_quantity = evaluator.max_quantity if evaluator.max_quantity.present?
19
+ rule.preferred_apply_to = evaluator.apply_to if evaluator.apply_to.present?
20
+ end
21
+
22
+ transient do
23
+ min_quantity { nil }
24
+ max_quantity { nil }
25
+ apply_to { nil }
26
+ end
27
+ end
28
+
29
+ factory :user_price_rule, class: Spree::PriceRules::UserRule do
30
+ after(:build) do |rule, evaluator|
31
+ rule.preferred_user_ids = evaluator.user_ids if evaluator.respond_to?(:user_ids)
32
+ end
33
+
34
+ transient do
35
+ user_ids { [] }
36
+ end
37
+ end
38
+
39
+ factory :customer_group_price_rule, class: Spree::PriceRules::CustomerGroupRule do
40
+ after(:build) do |rule, evaluator|
41
+ rule.preferred_customer_group_ids = evaluator.customer_group_ids if evaluator.respond_to?(:customer_group_ids)
42
+ end
43
+
44
+ transient do
45
+ customer_group_ids { [] }
46
+ end
47
+ end
48
+ end
49
+ end
@@ -18,4 +18,16 @@ FactoryBot.define do
18
18
  factory :promotion_rule_option_value, class: Spree::Promotion::Rules::OptionValue do
19
19
  association :promotion
20
20
  end
21
+
22
+ factory :promotion_rule_customer_group, class: Spree::Promotion::Rules::CustomerGroup do
23
+ association :promotion
24
+
25
+ after(:build) do |rule, evaluator|
26
+ rule.preferred_customer_group_ids = evaluator.customer_group_ids if evaluator.respond_to?(:customer_group_ids)
27
+ end
28
+
29
+ transient do
30
+ customer_group_ids { [] }
31
+ end
32
+ end
21
33
  end
@@ -5,10 +5,12 @@ FactoryBot.define do
5
5
  variant
6
6
 
7
7
  before(:create) do |stock_item|
8
- Spree::StockItem.find_by(
9
- variant: stock_item.variant,
10
- stock_location: stock_item.stock_location
11
- )&.destroy
8
+ # Use really_destroy! (hard delete) to avoid unique index conflicts
9
+ # with deleted_at timestamps in MySQL when tests run quickly
10
+ Spree::StockItem.where(
11
+ variant_id: stock_item.variant_id,
12
+ stock_location_id: stock_item.stock_location_id
13
+ ).delete_all
12
14
  end
13
15
 
14
16
  transient do
@@ -0,0 +1,6 @@
1
+ FactoryBot.define do
2
+ factory :store_product, class: Spree::StoreProduct do
3
+ store
4
+ product
5
+ end
6
+ end
@@ -3,7 +3,6 @@ FactoryBot.define do
3
3
  sequence(:name) { |n| "taxon_#{n}" }
4
4
 
5
5
  association :taxonomy, strategy: :create
6
- association :icon, factory: :taxon_image
7
6
  parent_id { taxonomy.root.id }
8
7
 
9
8
  trait :with_description do
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :webhook_delivery, class: Spree::WebhookDelivery do
5
+ webhook_endpoint
6
+ event_name { 'order.created' }
7
+ payload { { event: 'order.created', data: { id: 1 } } }
8
+
9
+ trait :pending do
10
+ response_code { nil }
11
+ execution_time { nil }
12
+ success { nil }
13
+ delivered_at { nil }
14
+ end
15
+
16
+ trait :successful do
17
+ response_code { 200 }
18
+ execution_time { 150 }
19
+ success { true }
20
+ delivered_at { Time.current }
21
+ end
22
+
23
+ trait :failed do
24
+ response_code { 500 }
25
+ execution_time { 200 }
26
+ success { false }
27
+ delivered_at { Time.current }
28
+ end
29
+
30
+ trait :timeout do
31
+ response_code { nil }
32
+ error_type { 'timeout' }
33
+ execution_time { 30_000 }
34
+ success { false }
35
+ request_errors { 'execution expired' }
36
+ delivered_at { Time.current }
37
+ end
38
+
39
+ trait :connection_error do
40
+ response_code { nil }
41
+ error_type { 'connection_error' }
42
+ execution_time { 100 }
43
+ success { false }
44
+ request_errors { 'Connection refused' }
45
+ delivered_at { Time.current }
46
+ end
47
+ end
48
+ end