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,253 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ # Concern for models that publish events.
5
+ #
6
+ # This concern is included in Spree::Base, so all Spree models
7
+ # can emit events. Event payloads are generated using dedicated
8
+ # serializers from the Spree::Events namespace.
9
+ #
10
+ # @example Disabling events for a specific model
11
+ # class Spree::LogEntry < Spree.base_class
12
+ # self.publish_events = false
13
+ # end
14
+ #
15
+ # @example Publishing custom events
16
+ # class Spree::Order < Spree.base_class
17
+ # def complete!
18
+ # # ... completion logic ...
19
+ # publish_event('order.completed')
20
+ # end
21
+ # end
22
+ #
23
+ # @example Creating an event serializer (required for each model that publishes events)
24
+ # # app/serializers/spree/events/order_serializer.rb
25
+ # class Spree::Events::OrderSerializer < Spree::Events::BaseSerializer
26
+ # protected
27
+ #
28
+ # def attributes
29
+ # {
30
+ # id: resource.id,
31
+ # number: resource.number,
32
+ # state: resource.state.to_s,
33
+ # # ... other attributes
34
+ # }
35
+ # end
36
+ # end
37
+ #
38
+ module Publishable
39
+ extend ActiveSupport::Concern
40
+
41
+ # Error raised when a model tries to publish an event but has no serializer defined
42
+ class MissingSerializerError < StandardError
43
+ def initialize(model_class)
44
+ serializer_name = "Spree::Events::#{model_class.name.demodulize}Serializer"
45
+ super(
46
+ "Missing event serializer for #{model_class.name}. " \
47
+ "Please create #{serializer_name} that inherits from Spree::Events::BaseSerializer. " \
48
+ "Example:\n\n" \
49
+ " class #{serializer_name} < Spree::Events::BaseSerializer\n" \
50
+ " protected\n\n" \
51
+ " def attributes\n" \
52
+ " {\n" \
53
+ " id: resource.id,\n" \
54
+ " # add other attributes here\n" \
55
+ " updated_at: timestamp(resource.updated_at)\n" \
56
+ " }\n" \
57
+ " end\n" \
58
+ " end"
59
+ )
60
+ end
61
+ end
62
+
63
+ included do
64
+ class_attribute :publish_events, default: true
65
+ class_attribute :lifecycle_events_enabled, default: false
66
+ end
67
+
68
+ class_methods do
69
+ # Enable automatic lifecycle event publishing
70
+ #
71
+ # @param options [Hash] Options for lifecycle events
72
+ # @option options [Array<Symbol>] :only Limit to specific events (:create, :update, :delete)
73
+ # @option options [Array<Symbol>] :except Exclude specific events
74
+ # @return [void]
75
+ #
76
+ # @example
77
+ # publishes_lifecycle_events
78
+ # publishes_lifecycle_events only: [:create, :delete]
79
+ # publishes_lifecycle_events except: [:update]
80
+ #
81
+ def publishes_lifecycle_events(options = {})
82
+ # Guard against duplicate callback registration (important for code reload in development)
83
+ return if lifecycle_events_enabled
84
+
85
+ self.lifecycle_events_enabled = true
86
+
87
+ events = [:create, :update, :delete]
88
+ events &= Array(options[:only]) if options[:only]
89
+ events -= Array(options[:except]) if options[:except]
90
+
91
+ if events.include?(:create)
92
+ after_commit :publish_create_event, on: :create, if: :should_publish_events?
93
+ end
94
+
95
+ if events.include?(:update)
96
+ after_commit :publish_update_event, on: :update, if: :should_publish_events?
97
+ end
98
+
99
+ if events.include?(:delete)
100
+ before_destroy :capture_pre_destroy_payload, if: :should_publish_events?
101
+ after_commit :publish_delete_event, on: :destroy, if: :should_publish_events?
102
+ end
103
+ end
104
+
105
+ # Disable lifecycle events for this model
106
+ #
107
+ # @example
108
+ # class Spree::LogEntry < Spree.base_class
109
+ # skip_lifecycle_events
110
+ # end
111
+ #
112
+ def skip_lifecycle_events
113
+ self.publish_events = false
114
+ end
115
+
116
+ # Get the event name prefix for this model
117
+ #
118
+ # If a parent class has explicitly set an event_prefix, it will be inherited.
119
+ # Otherwise, uses model_name.element (e.g., 'order' for Spree::Order)
120
+ #
121
+ # @return [String] e.g., 'order' for Spree::Order
122
+ def event_prefix
123
+ # If this class has an explicitly set prefix, use it
124
+ return @event_prefix if defined?(@event_prefix) && @event_prefix.present?
125
+
126
+ # Check if a parent class has an explicitly set prefix
127
+ parent = superclass
128
+ while parent && parent.respond_to?(:event_prefix)
129
+ if parent.instance_variable_defined?(:@event_prefix) && parent.instance_variable_get(:@event_prefix).present?
130
+ return parent.instance_variable_get(:@event_prefix)
131
+ end
132
+ parent = parent.superclass
133
+ end
134
+
135
+ # Default to model_name.element
136
+ @event_prefix ||= model_name.element
137
+ end
138
+
139
+ # Set a custom event prefix
140
+ #
141
+ # @param prefix [String]
142
+ def event_prefix=(prefix)
143
+ @event_prefix = prefix
144
+ end
145
+ end
146
+
147
+ # Publish an event with this model's data
148
+ #
149
+ # @param event_name [String] The event name (e.g., 'order.complete')
150
+ # @param payload [Hash, nil] Custom payload (defaults to event_payload)
151
+ # @param metadata [Hash] Additional metadata
152
+ # @return [Spree::Event] The published event
153
+ #
154
+ # @example
155
+ # order.publish_event('order.completed')
156
+ # order.publish_event('order.completed', { custom: 'data' })
157
+ # order.publish_event('order.completed', metadata: { user_id: 1 })
158
+ #
159
+ def publish_event(event_name, payload = nil, metadata = {})
160
+ return unless Spree::Events.enabled?
161
+
162
+ @_current_event_name = event_name
163
+ payload ||= event_payload
164
+ Spree::Events.publish(event_name, payload, metadata)
165
+ ensure
166
+ @_current_event_name = nil
167
+ end
168
+
169
+ # Get the payload for events
170
+ #
171
+ # Uses dedicated event serializer (Spree::Events::ModelSerializer).
172
+ # Raises MissingSerializerError if no serializer is defined.
173
+ #
174
+ # @return [Hash]
175
+ # @raise [MissingSerializerError] if no serializer is defined for this model
176
+ def event_payload
177
+ serializer = event_serializer_class
178
+
179
+ raise MissingSerializerError, self.class unless serializer
180
+
181
+ serializer.serialize(self, event_context)
182
+ end
183
+
184
+ # Find the event serializer class for this model
185
+ #
186
+ # Looks for Spree::Events::ModelNameSerializer (e.g., Spree::Events::OrderSerializer)
187
+ # Also walks up the class hierarchy to find a serializer for parent classes.
188
+ # This allows subclasses like Spree::Exports::Products to use ExportSerializer.
189
+ #
190
+ # @return [Class, nil] The serializer class or nil if not found
191
+ def event_serializer_class
192
+ return nil unless self.class.name
193
+
194
+ # Try this class and walk up the hierarchy
195
+ klass = self.class
196
+ while klass && klass != Object && klass != BasicObject
197
+ class_name = klass.name&.demodulize
198
+ # Skip looking for BaseSerializer - that's the parent class for all serializers
199
+ if class_name.present? && class_name != 'Base'
200
+ serializer = "Spree::Events::#{class_name}Serializer".safe_constantize
201
+ return serializer if serializer
202
+ end
203
+
204
+ klass = klass.superclass
205
+ end
206
+
207
+ nil
208
+ end
209
+
210
+ # Context passed to the event serializer
211
+ #
212
+ # @return [Hash]
213
+ def event_context
214
+ {
215
+ event_name: @_current_event_name,
216
+ store_id: Spree::Current.store&.id,
217
+ triggered_at: Time.current
218
+ }
219
+ end
220
+
221
+ # Get the event prefix for this instance
222
+ #
223
+ # @return [String]
224
+ def event_prefix
225
+ self.class.event_prefix
226
+ end
227
+
228
+ private
229
+
230
+ def should_publish_events?
231
+ self.class.publish_events && Spree::Events.enabled?
232
+ end
233
+
234
+ def publish_create_event
235
+ publish_event("#{event_prefix}.created")
236
+ end
237
+
238
+ def publish_update_event
239
+ publish_event("#{event_prefix}.updated")
240
+ end
241
+
242
+ def publish_delete_event
243
+ # For delete, we need to capture the data before it's gone
244
+ # The after_commit runs after the record is deleted, so we use
245
+ # the previously captured payload
246
+ publish_event("#{event_prefix}.deleted", @_pre_destroy_payload || event_payload)
247
+ end
248
+
249
+ def capture_pre_destroy_payload
250
+ @_pre_destroy_payload = event_payload
251
+ end
252
+ end
253
+ end
@@ -3,7 +3,7 @@ module Spree
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
- auto_strip_attributes :name
6
+ normalizes :name, with: ->(value) { value&.to_s&.squish&.presence }
7
7
 
8
8
  validates :name, presence: true,
9
9
  uniqueness: { case_sensitive: false, allow_blank: true, scope: spree_base_uniqueness_scope }
@@ -9,7 +9,12 @@ module Spree
9
9
  include Spree::AdminUserMethods
10
10
  include Spree::RansackableAttributes
11
11
  include Spree::MultiSearchable
12
+ include Spree::Publishable
13
+
12
14
  included do
15
+ # Enable lifecycle events for user models
16
+ publishes_lifecycle_events
17
+
13
18
  # we need to have this callback before any dependent: :destroy associations
14
19
  # https://github.com/rails/rails/issues/3458
15
20
  before_validation :clone_billing_address, if: :use_billing?
@@ -18,7 +23,7 @@ module Spree
18
23
  attr_accessor :use_billing
19
24
 
20
25
  has_person_name
21
- auto_strip_attributes :email, :first_name, :last_name
26
+ normalizes :email, :first_name, :last_name, with: ->(value) { value&.to_s&.squish&.presence }
22
27
  acts_as_taggable_on :tags
23
28
 
24
29
  #
@@ -33,6 +38,8 @@ module Spree
33
38
  has_many :wished_items, through: :wishlists, source: :wished_items
34
39
  has_many :gateway_customers, class_name: 'Spree::GatewayCustomer', foreign_key: :user_id
35
40
  has_many :gift_cards, class_name: 'Spree::GiftCard', foreign_key: :user_id, dependent: :destroy
41
+ has_many :customer_group_users, class_name: 'Spree::CustomerGroupUser', foreign_key: :user_id, as: :user, dependent: :destroy
42
+ has_many :customer_groups, through: :customer_group_users, class_name: 'Spree::CustomerGroup'
36
43
  belongs_to :ship_address, class_name: 'Spree::Address', optional: true
37
44
  belongs_to :bill_address, class_name: 'Spree::Address', optional: true
38
45
 
@@ -40,6 +47,7 @@ module Spree
40
47
  # Attachments
41
48
  #
42
49
  has_one_attached :avatar, service: Spree.public_storage_service_name
50
+ has_rich_text :internal_note
43
51
 
44
52
  #
45
53
  # Attributes
@@ -157,6 +165,18 @@ module Spree
157
165
  name&.full
158
166
  end
159
167
 
168
+ def event_serializer_class
169
+ Spree::Events::UserSerializer
170
+ end
171
+
172
+ def event_prefix
173
+ if self.class == Spree.admin_user_class && Spree.admin_user_class != Spree.user_class
174
+ 'admin'
175
+ else
176
+ 'user'
177
+ end
178
+ end
179
+
160
180
  private
161
181
 
162
182
  def check_completed_orders
@@ -166,9 +166,9 @@ module Spree
166
166
  end
167
167
  can :accept, Spree::Invitation, invitee_id: [user.id, nil], invitee_type: user.class.name, status: 'pending'
168
168
  can :read, ::Spree::Policy
169
- can :read, ::Spree::Page
170
- can :read, ::Spree::Post
171
- can :read, ::Spree::PostCategory
169
+ can :read, ::Spree::Page if defined?(Spree::Page)
170
+ can :read, ::Spree::Post if defined?(Spree::Post)
171
+ can :read, ::Spree::PostCategory if defined?(Spree::PostCategory)
172
172
  end
173
173
 
174
174
  def protect_admin_role
@@ -4,9 +4,6 @@ module Spree
4
4
 
5
5
  include Spree::Metafields
6
6
  include Spree::Metadata
7
- if defined?(Spree::Webhooks::HasWebhooks)
8
- include Spree::Webhooks::HasWebhooks
9
- end
10
7
 
11
8
  serialize :preferences, type: Hash, coder: YAML, default: {}
12
9
 
@@ -25,7 +25,7 @@ module Spree
25
25
  def add_adjustment(adjustment, opts = {})
26
26
  return unless adjustment.promotion?
27
27
 
28
- source = opts[:source] || adjustment.source
28
+ source = opts[:source] || adjustment.cached_source
29
29
  promotion = opts[:promotion] || source.promotion
30
30
 
31
31
  add(adjustments, adjustment, adjustment.id)
@@ -96,19 +96,45 @@ module Spree
96
96
  !included?
97
97
  end
98
98
 
99
+ # Returns the source using Rails.cache to avoid repeated database lookups.
100
+ # Sources are cached by their type and ID combination.
101
+ # Cache is automatically invalidated when the source is saved (see AdjustmentSource concern).
102
+ #
103
+ # @return [Object, nil] The source object (TaxRate, PromotionAction, etc.)
104
+ def cached_source
105
+ return nil if source_type.blank? || source_id.blank?
106
+
107
+ Rails.cache.fetch(source_cache_key) { source }
108
+ rescue TypeError
109
+ # Handle objects that can't be serialized (e.g., mock objects in tests)
110
+ source
111
+ end
112
+
113
+ # Cache key for the source object
114
+ def source_cache_key
115
+ "spree/adjustment_source/#{source_type}/#{source_id}"
116
+ end
117
+
99
118
  # Passing a target here would always be recommended as it would avoid
100
119
  # hitting the database again and would ensure you're compute values over
101
120
  # the specific object amount passed here.
102
121
  def update!(target = adjustable)
103
- return amount if closed? || source.blank?
122
+ src = cached_source
123
+ return amount if closed? || src.blank?
124
+
125
+ new_amount = src.compute_amount(target)
126
+ new_eligible = promotion? ? src.promotion.eligible?(target) : eligible
104
127
 
105
- amount = source.compute_amount(target)
106
- attributes = { amount: amount, updated_at: Time.current }
107
- attributes[:eligible] = source.promotion.eligible?(target) if promotion?
128
+ changed_attributes = {}
129
+ changed_attributes[:amount] = new_amount if new_amount != amount
130
+ changed_attributes[:eligible] = new_eligible if new_eligible != eligible
108
131
 
109
- update_columns(attributes)
132
+ if changed_attributes.any?
133
+ changed_attributes[:updated_at] = Time.current
134
+ update_columns(changed_attributes)
135
+ end
110
136
 
111
- amount
137
+ new_amount
112
138
  end
113
139
 
114
140
  private
@@ -3,9 +3,8 @@ module Spree
3
3
  include Support::ActiveStorage
4
4
  include Spree::Metafields
5
5
  include Spree::Metadata
6
- if defined?(Spree::Webhooks::HasWebhooks)
7
- include Spree::Webhooks::HasWebhooks
8
- end
6
+
7
+ publishes_lifecycle_events
9
8
 
10
9
  EXTERNAL_URL_METAFIELD_KEY = 'external.url'
11
10
 
@@ -63,5 +62,9 @@ module Spree
63
62
  def skip_import?
64
63
  false
65
64
  end
65
+
66
+ def event_prefix
67
+ 'asset'
68
+ end
66
69
  end
67
70
  end
@@ -3,6 +3,7 @@ class Spree::Base < ApplicationRecord
3
3
  include Spree::RansackableAttributes
4
4
  include Spree::TranslatableResourceScopes
5
5
  include Spree::IntegrationsConcern
6
+ include Spree::Publishable
6
7
 
7
8
  after_initialize do
8
9
  if has_attribute?(:preferences) && !preferences.nil?
@@ -10,7 +11,8 @@ class Spree::Base < ApplicationRecord
10
11
  end
11
12
  end
12
13
 
13
- if Kaminari.config.page_method_name != :page
14
+ # only for backwards compatibility with Kaminari
15
+ if defined?(Kaminari) && Kaminari.config.page_method_name != :page
14
16
  def self.page(num)
15
17
  send Kaminari.config.page_method_name, num
16
18
  end
@@ -4,8 +4,8 @@ module Spree
4
4
  acts_as_list scope: :taxon
5
5
 
6
6
  with_options inverse_of: :classifications, touch: true do
7
- belongs_to :product, class_name: 'Spree::Product'
8
- belongs_to :taxon, class_name: 'Spree::Taxon'
7
+ belongs_to :product, class_name: 'Spree::Product', counter_cache: :classification_count
8
+ belongs_to :taxon, class_name: 'Spree::Taxon', counter_cache: :classification_count
9
9
  end
10
10
 
11
11
  validates :taxon, :product, presence: true
@@ -5,9 +5,6 @@ module Spree
5
5
  include Spree::Metadata
6
6
  include Spree::PaymentSourceConcern
7
7
 
8
- if defined?(Spree::Webhooks::HasWebhooks)
9
- include Spree::Webhooks::HasWebhooks
10
- end
11
8
  if defined?(Spree::Security::CreditCards)
12
9
  include Spree::Security::CreditCards
13
10
  end
@@ -1,13 +1,34 @@
1
1
  module Spree
2
2
  class Current < ::ActiveSupport::CurrentAttributes
3
- attribute :store, :webhooks_subscribers
3
+ attribute :store, :currency, :zone, :price_lists, :global_pricing_context
4
4
 
5
5
  def store
6
6
  super || Spree::Store.default
7
7
  end
8
8
 
9
- def webhooks_subscribers
10
- super || store&.active_webhooks_subscribers || Spree::Webhooks::Subscriber.none
9
+ def currency
10
+ super || store&.default_currency
11
+ end
12
+
13
+ def zone
14
+ super || Spree::Zone.default_tax || store&.checkout_zone
15
+ end
16
+
17
+ def price_lists
18
+ super || begin
19
+ context = global_pricing_context
20
+ self.price_lists = Spree::PriceList.for_context(context)
21
+ end
22
+ end
23
+
24
+ def global_pricing_context
25
+ super || begin
26
+ self.global_pricing_context = Spree::Pricing::Context.new(
27
+ currency: currency,
28
+ store: store,
29
+ zone: zone
30
+ )
31
+ end
11
32
  end
12
33
  end
13
34
  end
@@ -4,7 +4,7 @@ module Spree
4
4
  include Spree::Metafields
5
5
  include Spree::Metadata
6
6
 
7
- auto_strip_attributes :url
7
+ normalizes :url, with: ->(value) { value&.to_s&.squish&.presence }
8
8
 
9
9
  #
10
10
  # Associations
@@ -0,0 +1,94 @@
1
+ module Spree
2
+ class CustomerGroup < Spree.base_class
3
+ acts_as_paranoid
4
+
5
+ #
6
+ # Associations
7
+ #
8
+ belongs_to :store, class_name: 'Spree::Store', inverse_of: :customer_groups
9
+ has_many :customer_group_users, class_name: 'Spree::CustomerGroupUser', dependent: :destroy
10
+ has_many :users, through: :customer_group_users, source: :user, source_type: Spree.user_class.to_s
11
+
12
+ #
13
+ # Validations
14
+ #
15
+ validates :name, presence: true, uniqueness: { scope: [:store_id], conditions: -> { where(deleted_at: nil) } }
16
+ validates :store, presence: true
17
+
18
+ #
19
+ # Scopes
20
+ #
21
+ scope :for_store, ->(store) { where(store: store) }
22
+
23
+ #
24
+ # Instance Methods
25
+ #
26
+ def users_count
27
+ customer_group_users.count
28
+ end
29
+
30
+ # Bulk add customers to the group
31
+ # @param user_ids [Array] array of user IDs to add
32
+ # @return [Integer] number of customers added
33
+ def add_customers(user_ids)
34
+ return 0 if user_ids.blank?
35
+
36
+ user_ids = Array(user_ids).map(&:to_s).uniq
37
+ return 0 if user_ids.empty?
38
+
39
+ # Get existing user IDs to avoid duplicates
40
+ existing_user_ids = customer_group_users.where(user_id: user_ids).pluck(:user_id).map(&:to_s).to_set
41
+
42
+ now = Time.current
43
+ user_type = Spree.user_class.to_s
44
+
45
+ records_to_insert = user_ids.filter_map do |user_id|
46
+ next if existing_user_ids.include?(user_id)
47
+
48
+ {
49
+ customer_group_id: id,
50
+ user_id: user_id,
51
+ user_type: user_type,
52
+ created_at: now,
53
+ updated_at: now
54
+ }
55
+ end
56
+
57
+ return 0 if records_to_insert.empty?
58
+
59
+ Spree::CustomerGroupUser.upsert_all(records_to_insert, on_duplicate: :skip)
60
+ added_user_ids = records_to_insert.map { |r| r[:user_id] }
61
+ touch_users(added_user_ids)
62
+ touch
63
+
64
+ records_to_insert.size
65
+ end
66
+
67
+ # Bulk remove customers from the group
68
+ # @param user_ids [Array] array of user IDs to remove
69
+ # @return [Integer] number of customers removed
70
+ def remove_customers(user_ids)
71
+ return 0 if user_ids.blank?
72
+
73
+ user_ids = Array(user_ids).map(&:to_s).uniq
74
+ return 0 if user_ids.empty?
75
+
76
+ deleted_count = customer_group_users.where(user_id: user_ids).delete_all
77
+
78
+ if deleted_count > 0
79
+ touch_users(user_ids)
80
+ touch
81
+ end
82
+
83
+ deleted_count
84
+ end
85
+
86
+ private
87
+
88
+ def touch_users(user_ids)
89
+ return if user_ids.blank?
90
+
91
+ Spree.user_class.where(id: user_ids).touch_all
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,16 @@
1
+ module Spree
2
+ class CustomerGroupUser < Spree.base_class
3
+ #
4
+ # Associations
5
+ #
6
+ belongs_to :customer_group, class_name: 'Spree::CustomerGroup'
7
+ belongs_to :user, polymorphic: true
8
+
9
+ #
10
+ # Validations
11
+ #
12
+ validates :customer_group, presence: true
13
+ validates :user, presence: true
14
+ validates :customer_group_id, uniqueness: { scope: [:user_id, :user_type] }
15
+ end
16
+ end
@@ -4,9 +4,8 @@ module Spree
4
4
  include Spree::NumberIdentifier
5
5
  include Spree::Metafields
6
6
  include Spree::Metadata
7
- if defined?(Spree::Webhooks::HasWebhooks)
8
- include Spree::Webhooks::HasWebhooks
9
- end
7
+
8
+ publishes_lifecycle_events
10
9
 
11
10
  belongs_to :stock_location
12
11
  belongs_to :store, class_name: 'Spree::Store', inverse_of: :customer_returns
@@ -1,12 +1,10 @@
1
1
  module Spree
2
2
  class Digital < Spree.base_class
3
+ publishes_lifecycle_events
4
+
3
5
  belongs_to :variant
4
6
  has_many :digital_links, dependent: :destroy
5
7
 
6
- if defined?(Spree::Webhooks::HasWebhooks)
7
- include Spree::Webhooks::HasWebhooks
8
- end
9
-
10
8
  has_one_attached :attachment, service: Spree.private_storage_service_name
11
9
 
12
10
  validates :attachment, attached: true
@@ -1,14 +1,13 @@
1
1
  module Spree
2
2
  class DigitalLink < Spree.base_class
3
+ publishes_lifecycle_events
4
+
3
5
  if Rails::VERSION::STRING >= '7.1.0'
4
6
  has_secure_token on: :save
5
7
  else
6
8
  has_secure_token
7
9
  end
8
10
 
9
- if defined?(Spree::Webhooks::HasWebhooks)
10
- include Spree::Webhooks::HasWebhooks
11
- end
12
11
  if defined?(Spree::Security::DigitalLinks)
13
12
  include Spree::Security::DigitalLinks
14
13
  end