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,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ # Base class for event subscribers.
5
+ #
6
+ # Subscribers handle events published through the Spree event system.
7
+ # They provide a clean DSL for declaring which events to subscribe to
8
+ # and are automatically registered during Rails initialization.
9
+ #
10
+ # @example Basic subscriber
11
+ # class OrderCompletedNotifier < Spree::Subscriber
12
+ # subscribes_to 'order.complete'
13
+ #
14
+ # def call(event)
15
+ # order_id = event.payload['id']
16
+ # Spree::OrderMailer.confirm_email(order_id).deliver_later
17
+ # end
18
+ # end
19
+ #
20
+ # @example Multi-event subscriber
21
+ # class OrderAuditLogger < Spree::Subscriber
22
+ # subscribes_to 'order.complete', 'order.cancel', 'order.resume'
23
+ #
24
+ # def call(event)
25
+ # AuditLog.create!(
26
+ # event_name: event.name,
27
+ # payload: event.payload,
28
+ # occurred_at: event.timestamp
29
+ # )
30
+ # end
31
+ # end
32
+ #
33
+ # @example Pattern matching subscriber
34
+ # class OrderEventLogger < Spree::Subscriber
35
+ # subscribes_to 'order.*'
36
+ #
37
+ # def call(event)
38
+ # Rails.logger.info("Order event: #{event.name}")
39
+ # end
40
+ # end
41
+ #
42
+ # @example Subscriber with method routing
43
+ # class PaymentSubscriber < Spree::Subscriber
44
+ # subscribes_to 'payment.complete', 'payment.void', 'payment.refund'
45
+ #
46
+ # on 'payment.complete', :handle_complete
47
+ # on 'payment.void', :handle_void
48
+ # on 'payment.refund', :handle_refund
49
+ #
50
+ # private
51
+ #
52
+ # def handle_complete(event)
53
+ # # Handle payment completion
54
+ # end
55
+ #
56
+ # def handle_void(event)
57
+ # # Handle payment void
58
+ # end
59
+ #
60
+ # def handle_refund(event)
61
+ # # Handle payment refund
62
+ # end
63
+ # end
64
+ #
65
+ # @example Synchronous subscriber (runs immediately, not via ActiveJob)
66
+ # class CriticalOrderHandler < Spree::Subscriber
67
+ # subscribes_to 'order.complete', async: false
68
+ #
69
+ # def call(event)
70
+ # # This runs synchronously
71
+ # end
72
+ # end
73
+ #
74
+ class Subscriber
75
+ class << self
76
+ # DSL method to declare which events this subscriber handles
77
+ #
78
+ # @param patterns [Array<String>] Event patterns to subscribe to
79
+ # @param options [Hash] Subscription options
80
+ # @option options [Boolean] :async (true) Whether to run async via ActiveJob
81
+ # @return [void]
82
+ #
83
+ # @example
84
+ # subscribes_to 'order.complete'
85
+ # subscribes_to 'order.complete', 'order.cancel'
86
+ # subscribes_to 'order.*'
87
+ # subscribes_to 'order.complete', async: false
88
+ #
89
+ def subscribes_to(*patterns, **options)
90
+ @subscription_patterns ||= []
91
+ @subscription_options = options
92
+
93
+ patterns.flatten.each do |pattern|
94
+ @subscription_patterns << pattern.to_s
95
+ end
96
+ end
97
+
98
+ # DSL method to route specific events to specific methods
99
+ #
100
+ # @param pattern [String] Event pattern
101
+ # @param method_name [Symbol] Method to call for this event
102
+ # @return [void]
103
+ #
104
+ # @example
105
+ # on 'payment.complete', :handle_complete
106
+ # on 'payment.void', :handle_void
107
+ #
108
+ def on(pattern, method_name)
109
+ @event_handlers ||= {}
110
+ @event_handlers[pattern.to_s] = method_name
111
+ end
112
+
113
+ # Get all subscription patterns for this subscriber
114
+ #
115
+ # @return [Array<String>]
116
+ def subscription_patterns
117
+ @subscription_patterns ||= []
118
+ end
119
+
120
+ # Get subscription options
121
+ #
122
+ # @return [Hash]
123
+ def subscription_options
124
+ @subscription_options ||= {}
125
+ end
126
+
127
+ # Get event handlers mapping
128
+ #
129
+ # @return [Hash<String, Symbol>]
130
+ def event_handlers
131
+ @event_handlers ||= {}
132
+ end
133
+
134
+ # Class-level call method for when the class itself is used as subscriber
135
+ #
136
+ # @param event [Spree::Event]
137
+ def call(event)
138
+ new.call(event)
139
+ end
140
+ end
141
+
142
+ # Handle an event
143
+ #
144
+ # Override this method in subclasses to handle events.
145
+ # If you've defined event handlers with `on`, this method
146
+ # will route to the appropriate handler automatically.
147
+ #
148
+ # @param event [Spree::Event] The event to handle
149
+ # @return [void]
150
+ def call(event)
151
+ handler = find_handler(event)
152
+
153
+ if handler
154
+ send(handler, event)
155
+ else
156
+ # Default behavior - subclasses should override
157
+ handle(event)
158
+ end
159
+ end
160
+
161
+ # Default event handler
162
+ #
163
+ # Override this in subclasses if not using the `on` DSL
164
+ #
165
+ # @param event [Spree::Event]
166
+ def handle(event)
167
+ # Override in subclass
168
+ end
169
+
170
+ private
171
+
172
+ def find_handler(event)
173
+ handlers = self.class.event_handlers
174
+
175
+ # Try exact match first
176
+ return handlers[event.name] if handlers.key?(event.name)
177
+
178
+ # Try pattern matching
179
+ handlers.each do |pattern, method_name|
180
+ return method_name if event.matches?(pattern)
181
+ end
182
+
183
+ nil
184
+ end
185
+ end
186
+ end
@@ -1,9 +1,5 @@
1
1
  module Spree
2
2
  class TaxCategory < Spree.base_class
3
- if defined?(Spree::Webhooks::HasWebhooks)
4
- include Spree::Webhooks::HasWebhooks
5
- end
6
-
7
3
  acts_as_paranoid
8
4
  validates :name, presence: true, uniqueness: { case_sensitive: false, scope: spree_base_uniqueness_scope + [:deleted_at] }
9
5
 
@@ -6,9 +6,6 @@ module Spree
6
6
  include Spree::AdjustmentSource
7
7
  include Spree::Metafields
8
8
  include Spree::Metadata
9
- if defined?(Spree::Webhooks::HasWebhooks)
10
- include Spree::Webhooks::HasWebhooks
11
- end
12
9
 
13
10
  with_options inverse_of: :tax_rates do
14
11
  belongs_to :zone, class_name: 'Spree::Zone', optional: true
@@ -19,10 +19,6 @@ module Spree
19
19
  include Spree::Metafields
20
20
  include Spree::Metadata
21
21
  include Spree::MemoizedData
22
- include Spree::Linkable
23
- if defined?(Spree::Webhooks::HasWebhooks)
24
- include Spree::Webhooks::HasWebhooks
25
- end
26
22
 
27
23
  MEMOIZED_METHODS = %w[cached_self_and_descendants_ids].freeze
28
24
 
@@ -31,7 +27,7 @@ module Spree
31
27
  #
32
28
  extend FriendlyId
33
29
  friendly_id :permalink, slug_column: :permalink, use: :history
34
- acts_as_nested_set dependent: :destroy
30
+ acts_as_nested_set dependent: :destroy, counter_cache: :children_count
35
31
 
36
32
  #
37
33
  # Associations
@@ -85,10 +81,6 @@ module Spree
85
81
  after_move :regenerate_pretty_name_and_permalink
86
82
  after_move :regenerate_translations_pretty_name_and_permalink
87
83
 
88
- after_commit :touch_featured_sections, on: [:update]
89
- after_touch :touch_featured_sections
90
- after_destroy :remove_featured_sections, if: -> { featured? }
91
-
92
84
  #
93
85
  # Scopes
94
86
  #
@@ -167,10 +159,6 @@ module Spree
167
159
  sort_order == 'manual'
168
160
  end
169
161
 
170
- def page_builder_image
171
- square_image.presence || image
172
- end
173
-
174
162
  def active_products_with_descendants
175
163
  @active_products_with_descendants ||= store.products.
176
164
  joins(:classifications).
@@ -324,7 +312,7 @@ module Spree
324
312
  # indicate which filters should be used for a taxon
325
313
  # this method should be customized to your own site
326
314
  def applicable_filters
327
- Spree::Deprecation.warn('applicable_filters is deprecated and will be removed in Spree 6.0')
315
+ Spree::Deprecation.warn('applicable_filters is deprecated and will be removed in Spree 5.5')
328
316
  fs = []
329
317
  # fs << ProductFilters.taxons_below(self)
330
318
  ## unless it's a root taxon? left open for demo purposes
@@ -366,6 +354,7 @@ module Spree
366
354
  end
367
355
 
368
356
  def active_products
357
+ Spree::Deprecation.warn('active_products is deprecated and will be removed in Spree 5.5. Please use taxon.products.active instead.')
369
358
  products.active
370
359
  end
371
360
 
@@ -401,20 +390,6 @@ module Spree
401
390
  move_to_child_with_index(parent, idx.to_i) unless new_record?
402
391
  end
403
392
 
404
- def page_builder_url
405
- return unless Spree::Core::Engine.routes.url_helpers.respond_to?(:nested_taxons_path)
406
-
407
- Spree::Core::Engine.routes.url_helpers.nested_taxons_path(self)
408
- end
409
-
410
- def featured?
411
- featured_sections.any?
412
- end
413
-
414
- def featured_sections
415
- @featured_sections ||= Spree::PageSections::FeaturedTaxon.published.by_taxon_id(id)
416
- end
417
-
418
393
  private
419
394
 
420
395
  def should_regenerate_pretty_name_and_permalink?
@@ -455,13 +430,5 @@ module Spree
455
430
  def regenerate_translations_pretty_name_and_permalink
456
431
  translations.each(&:regenerate_pretty_name_and_permalink)
457
432
  end
458
-
459
- def touch_featured_sections
460
- Spree::Taxons::TouchFeaturedSections.call(taxon_ids: [id])
461
- end
462
-
463
- def remove_featured_sections
464
- featured_sections.destroy_all
465
- end
466
433
  end
467
434
  end
@@ -4,9 +4,6 @@ module Spree
4
4
  include Spree::Metafields
5
5
  include Spree::Metadata
6
6
  include Spree::SingleStoreResource
7
- if defined?(Spree::Webhooks::HasWebhooks)
8
- include Spree::Webhooks::HasWebhooks
9
- end
10
7
 
11
8
  TRANSLATABLE_FIELDS = %i[name].freeze
12
9
  translates(*TRANSLATABLE_FIELDS, column_fallback: !Spree.always_use_translations?)
@@ -8,6 +8,8 @@ module Spree
8
8
  include Spree::Metadata
9
9
  include Spree::Variant::Webhooks
10
10
 
11
+ publishes_lifecycle_events
12
+
11
13
  MEMOIZED_METHODS = %w(purchasable in_stock on_sale backorderable tax_category options_text compare_at_price)
12
14
 
13
15
  DIMENSION_UNITS = %w[mm cm in ft]
@@ -19,7 +21,7 @@ module Spree
19
21
  delegate :name, :name=, :description, :slug, :available_on, :make_active_at, :shipping_category_id,
20
22
  :meta_description, :meta_keywords, :shipping_category, to: :product
21
23
 
22
- auto_strip_attributes :sku, nullify: false
24
+ normalizes :sku, with: ->(value) { value&.to_s&.strip }
23
25
 
24
26
  # we need to have this callback before any dependent: :destroy associations
25
27
  # https://github.com/rails/rails/issues/3458
@@ -80,6 +82,8 @@ module Spree
80
82
 
81
83
  after_commit :remove_prices_from_master_variant, on: [:create, :update], unless: :is_master?
82
84
  after_commit :remove_stock_items_from_master_variant, on: :create, unless: :is_master?
85
+ after_create :increment_product_variant_count, unless: :is_master?
86
+ after_destroy :decrement_product_variant_count, unless: :is_master?
83
87
 
84
88
  after_touch :clear_in_stock_cache
85
89
 
@@ -208,6 +212,8 @@ module Spree
208
212
  product_name_or_sku_cont(query)
209
213
  end
210
214
 
215
+ # Returns the human name of the variant.
216
+ # @return [String] the human name of the variant
211
217
  def human_name
212
218
  @human_name ||= option_values.
213
219
  joins(option_type: :product_option_types).
@@ -216,10 +222,14 @@ module Spree
216
222
  pluck(:presentation).join('/')
217
223
  end
218
224
 
225
+ # Returns true if the variant is available.
226
+ # @return [Boolean] true if the variant is available
219
227
  def available?
220
228
  !discontinued? && product.available?
221
229
  end
222
230
 
231
+ # Returns true if the variant is in stock or backorderable.
232
+ # @return [Boolean] true if the variant is in stock or backorderable
223
233
  def in_stock_or_backorderable?
224
234
  self.class.in_stock_or_backorderable.exists?(id: id)
225
235
  end
@@ -244,6 +254,8 @@ module Spree
244
254
  end
245
255
  end
246
256
 
257
+ # Returns the options text of the variant.
258
+ # @return [String] the options text of the variant
247
259
  def options_text
248
260
  @options_text ||= if option_values.loaded?
249
261
  option_values.sort_by { |ov| ov.option_type.position }.map { |ov| "#{ov.option_type.presentation}: #{ov.presentation}" }.to_sentence(words_connector: ', ', two_words_connector: ', ')
@@ -252,11 +264,14 @@ module Spree
252
264
  end
253
265
  end
254
266
 
255
- # Default to master name
267
+ # Returns the exchange name of the variant.
268
+ # @return [String] the exchange name of the variant
256
269
  def exchange_name
257
270
  is_master? ? name : options_text
258
271
  end
259
272
 
273
+ # Returns the descriptive name of the variant.
274
+ # @return [String] the descriptive name of the variant
260
275
  def descriptive_name
261
276
  is_master? ? name + ' - Master' : name + ' - ' + options_text
262
277
  end
@@ -264,34 +279,42 @@ module Spree
264
279
  # use deleted? rather than checking the attribute directly. this
265
280
  # allows extensions to override deleted? if they want to provide
266
281
  # their own definition.
282
+ # @return [Boolean] true if the variant is deleted.
267
283
  def deleted?
268
284
  !!deleted_at
269
285
  end
270
286
 
271
- # Returns default Image for Variant
272
- # @return [Spree::Image]
287
+ # Returns true if the variant has images.
288
+ # Uses loaded association when available, otherwise falls back to counter cache.
289
+ # @return [Boolean]
290
+ def has_images?
291
+ return images.any? if images.loaded?
292
+
293
+ image_count.positive?
294
+ end
295
+
296
+ # Returns default Image for Variant, falling back to product's default image.
297
+ # @return [Spree::Image, nil]
273
298
  def default_image
274
- @default_image ||= if images.any?
275
- images.first
276
- else
277
- product.default_image
278
- end
299
+ @default_image ||= has_images? ? images.first : product.default_image
300
+ end
301
+
302
+ # Returns first Image for Variant.
303
+ # @return [Spree::Image, nil]
304
+ def primary_image
305
+ images.first
279
306
  end
280
307
 
281
- # Returns secondary Image for Variant
282
- # @return [Spree::Image]
308
+ # Returns second Image for Variant (for hover effects).
309
+ # @return [Spree::Image, nil]
283
310
  def secondary_image
284
- @secondary_image ||= if images.size > 1
285
- images.second
286
- else
287
- product.secondary_image
288
- end
311
+ images.second
289
312
  end
290
313
 
291
- # Returns additional Images for Variant
314
+ # Returns all images except the default image, combining variant and product images.
292
315
  # @return [Array<Spree::Image>]
293
316
  def additional_images
294
- @additional_images ||= (images + product.images).uniq.find_all { |image| image.id != default_image&.id }
317
+ @additional_images ||= (images + product.images).uniq.reject { |image| image.id == default_image&.id }
295
318
  end
296
319
 
297
320
  # Returns an array of hashes with the option type name, value and presentation
@@ -310,6 +333,9 @@ module Spree
310
333
  end
311
334
  end
312
335
 
336
+ # Sets the option values for the variant
337
+ # @param options [Array<Hash>] the options to set
338
+ # @return [void]
313
339
  def options=(options = {})
314
340
  options.each do |option|
315
341
  next if option[:name].blank? || option[:value].blank?
@@ -318,6 +344,11 @@ module Spree
318
344
  end
319
345
  end
320
346
 
347
+ # Sets the option value for the given option name.
348
+ # @param opt_name [String] the option name to set the option value for
349
+ # @param opt_value [String] the option value to set
350
+ # @param opt_type_position [Integer] the position of the option type
351
+ # @return [void]
321
352
  def set_option_value(opt_name, opt_value, opt_type_position = nil)
322
353
  # no option values on master
323
354
  return if is_master
@@ -354,10 +385,16 @@ module Spree
354
385
  save
355
386
  end
356
387
 
388
+ # Returns the option value for the given option name.
389
+ # @param opt_name [String] the option name to get the option value for
390
+ # @return [Spree::OptionValue] the option value for the given option name
357
391
  def find_option_value(opt_name)
358
392
  option_values.includes(:option_type).detect { |o| o.option_type.name.parameterize == opt_name.parameterize }
359
393
  end
360
394
 
395
+ # Returns the presentation of the option value for the given option type.
396
+ # @param option_type [Spree::OptionType] the option type to get the option value for
397
+ # @return [String] the presentation of the option value for the given option type
361
398
  def option_value(option_type)
362
399
  if option_type.is_a?(Spree::OptionType)
363
400
  option_values.detect { |o| o.option_type_id == option_type.id }.try(:presentation)
@@ -366,13 +403,17 @@ module Spree
366
403
  end
367
404
  end
368
405
 
406
+ # Returns the base price (global price, not from a price list) for the given currency.
407
+ # Use price_for(context) when you need to resolve prices including price lists.
408
+ # @param currency [String] the currency to get the price for
409
+ # @return [Spree::Price] the base price for the given currency
369
410
  def price_in(currency)
370
411
  currency = currency&.upcase
371
412
 
372
413
  price = if prices.loaded? && prices.any?
373
- prices.detect { |p| p.currency == currency }
414
+ prices.detect { |p| p.currency == currency && p.price_list_id.nil? }
374
415
  else
375
- prices.find_by(currency: currency)
416
+ prices.base_prices.find_by(currency: currency)
376
417
  end
377
418
 
378
419
  if price.nil?
@@ -390,21 +431,52 @@ module Spree
390
431
  )
391
432
  end
392
433
 
434
+ # Returns the amount for the given currency.
435
+ # @param currency [String] the currency to get the amount for
436
+ # @return [BigDecimal] the amount for the given currency
393
437
  def amount_in(currency)
394
438
  price_in(currency).try(:amount)
395
439
  end
396
440
 
441
+ # Returns the compare at amount for the given currency.
442
+ # @param currency [String] the currency to get the compare at amount for
443
+ # @return [BigDecimal] the compare at amount for the given currency
397
444
  def compare_at_amount_in(currency)
398
445
  price_in(currency).try(:compare_at_amount)
399
446
  end
400
447
 
448
+ # Sets the base price (global price, not for a price list) for the given currency.
449
+ # @param currency [String] the currency to set the price for
450
+ # @param amount [BigDecimal] the amount to set
451
+ # @param compare_at_amount [BigDecimal] the compare at amount to set
452
+ # @return [void]
401
453
  def set_price(currency, amount, compare_at_amount = nil)
402
- price = prices.find_or_initialize_by(currency: currency)
454
+ price = prices.base_prices.find_or_initialize_by(currency: currency)
403
455
  price.amount = amount
404
456
  price.compare_at_amount = compare_at_amount if compare_at_amount.present?
405
457
  price.save!
406
458
  end
407
459
 
460
+ # Returns the price for the given context or options.
461
+ # @param context_or_options [Spree::Pricing::Context|Hash] the context or options to get the price for
462
+ # @return [Spree::Price] the price for the given context or options
463
+ def price_for(context_or_options)
464
+ context = if context_or_options.is_a?(Spree::Pricing::Context)
465
+ context_or_options
466
+ elsif context_or_options.is_a?(Hash)
467
+ Spree::Pricing::Context.new(**context_or_options.merge(variant: self))
468
+ else
469
+ raise ArgumentError, 'Must provide a Pricing::Context or options hash'
470
+ end
471
+
472
+ Spree::Pricing::Resolver.new(context).resolve
473
+ end
474
+
475
+ # Sets the stock for the variant
476
+ # @param count_on_hand [Integer] the count on hand
477
+ # @param backorderable [Boolean] the backorderable flag
478
+ # @param stock_location [Spree::StockLocation] the stock location to set the stock for
479
+ # @return [void]
408
480
  def set_stock(count_on_hand, backorderable = nil, stock_location = nil)
409
481
  stock_location ||= Spree::Store.current.default_stock_location
410
482
  stock_item = stock_items.find_or_initialize_by(stock_location: stock_location)
@@ -426,6 +498,9 @@ module Spree
426
498
  end.sum
427
499
  end
428
500
 
501
+ # Returns the price modifier amount of the variant.
502
+ # @param options [Hash] the options to get the price modifier amount for
503
+ # @return [BigDecimal] the price modifier amount of the variant
429
504
  def price_modifier_amount(options = {})
430
505
  return 0 unless options.present?
431
506
 
@@ -439,18 +514,26 @@ module Spree
439
514
  end.sum
440
515
  end
441
516
 
517
+ # Returns the compare at price of the variant.
518
+ # @return [BigDecimal] the compare at price of the variant
442
519
  def compare_at_price
443
520
  @compare_at_price ||= price_in(cost_currency).try(:compare_at_amount)
444
521
  end
445
522
 
523
+ # Returns the name and sku of the variant.
524
+ # @return [String] the name and sku of the variant
446
525
  def name_and_sku
447
526
  "#{name} - #{sku}"
448
527
  end
449
528
 
529
+ # Returns the sku and options text of the variant.
530
+ # @return [String] the sku and options text of the variant
450
531
  def sku_and_options_text
451
532
  "#{sku} #{options_text}".strip
452
533
  end
453
534
 
535
+ # Returns true if the variant is in stock.
536
+ # @return [Boolean] true if the variant is in stock
454
537
  def in_stock?
455
538
  @in_stock ||= if association(:stock_items).loaded? && association(:stock_locations).loaded?
456
539
  total_on_hand.positive?
@@ -461,6 +544,8 @@ module Spree
461
544
  end
462
545
  end
463
546
 
547
+ # Returns true if the variant is backorderable.
548
+ # @return [Boolean] true if the variant is backorderable
464
549
  def backorderable?
465
550
  @backorderable ||= Rails.cache.fetch(['variant-backorderable', cache_key_with_version]) do
466
551
  quantifier.backorderable?
@@ -609,5 +694,13 @@ module Spree
609
694
  def remove_stock_items_from_master_variant
610
695
  product.master.stock_items.delete_all
611
696
  end
697
+
698
+ def increment_product_variant_count
699
+ Spree::Product.increment_counter(:variant_count, product_id)
700
+ end
701
+
702
+ def decrement_product_variant_count
703
+ Spree::Product.decrement_counter(:variant_count, product_id)
704
+ end
612
705
  end
613
706
  end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ class WebhookDelivery < Spree.base_class
5
+ belongs_to :webhook_endpoint, class_name: 'Spree::WebhookEndpoint'
6
+ delegate :url, to: :webhook_endpoint
7
+
8
+ validates :event_name, presence: true
9
+ validates :payload, presence: true
10
+
11
+ ERROR_TYPES = %w[timeout connection_error].freeze
12
+
13
+ scope :successful, -> { where(success: true) }
14
+ scope :failed, -> { where(success: false) }
15
+ scope :pending, -> { where(delivered_at: nil) }
16
+ scope :recent, -> { order(created_at: :desc) }
17
+ scope :for_event, ->(event_name) { where(event_name: event_name) }
18
+
19
+ # Ransack configuration
20
+ self.whitelisted_ransackable_attributes = %w[event_name response_code execution_time success delivered_at]
21
+
22
+ # Check if the delivery was successful
23
+ #
24
+ # @return [Boolean]
25
+ def successful?
26
+ success == true
27
+ end
28
+
29
+ # Check if the delivery failed
30
+ #
31
+ # @return [Boolean]
32
+ def failed?
33
+ success == false
34
+ end
35
+
36
+ # Check if the delivery is pending
37
+ #
38
+ # @return [Boolean]
39
+ def pending?
40
+ delivered_at.nil?
41
+ end
42
+
43
+ # Mark delivery as completed with HTTP response
44
+ #
45
+ # @param response_code [Integer] HTTP response code
46
+ # @param execution_time [Integer] time in milliseconds
47
+ # @param response_body [String] response body from the webhook endpoint
48
+ def complete!(response_code: nil, execution_time:, error_type: nil, request_errors: nil, response_body: nil)
49
+ update!(
50
+ response_code: response_code,
51
+ execution_time: execution_time,
52
+ error_type: error_type,
53
+ request_errors: request_errors,
54
+ response_body: response_body,
55
+ success: response_code.present? && response_code.to_s.start_with?('2'),
56
+ delivered_at: Time.current
57
+ )
58
+ end
59
+ end
60
+ end