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
@@ -22,7 +22,7 @@ module Spree
22
22
  class Product < Spree.base_class
23
23
  acts_as_paranoid
24
24
  acts_as_taggable_on :tags, :labels
25
- auto_strip_attributes :name
25
+ normalizes :name, with: ->(value) { value&.to_s&.squish&.presence }
26
26
 
27
27
  include Spree::ProductScopes
28
28
  include Spree::MultiStoreResource
@@ -30,17 +30,20 @@ module Spree
30
30
  include Spree::MemoizedData
31
31
  include Spree::Metafields
32
32
  include Spree::Metadata
33
- include Spree::Linkable
34
33
  include Spree::Product::Webhooks
35
34
  include Spree::Product::Slugs
36
35
  if defined?(Spree::VendorConcern)
37
36
  include Spree::VendorConcern
38
37
  end
39
38
 
39
+ publishes_lifecycle_events
40
+
40
41
  MEMOIZED_METHODS = %w[total_on_hand taxonomy_ids taxon_and_ancestors category
41
- default_variant_id tax_category default_variant
42
- default_image secondary_image
43
- purchasable? in_stock? backorderable? has_variants? digital?]
42
+ default_variant_id tax_category default_variant variant_for_images
43
+ category_taxon brand_taxon main_taxon
44
+ purchasable? in_stock? backorderable? digital?]
45
+
46
+ STATUSES = %w[draft active archived].freeze
44
47
 
45
48
  STATUS_TO_WEBHOOK_EVENT = {
46
49
  'active' => 'activated',
@@ -52,6 +55,8 @@ module Spree
52
55
  translates(*TRANSLATABLE_FIELDS, column_fallback: !Spree.always_use_translations?)
53
56
 
54
57
  self::Translation.class_eval do
58
+ normalizes :name, :meta_title, with: ->(value) { value&.to_s&.squish&.presence }
59
+
55
60
  if defined?(PgSearch)
56
61
  include PgSearch::Model
57
62
 
@@ -107,6 +112,7 @@ module Spree
107
112
 
108
113
  has_many :line_items, through: :variants_including_master
109
114
  has_many :orders, through: :line_items
115
+ has_many :completed_orders, -> { reorder(nil).distinct.complete }, through: :line_items, source: :order
110
116
 
111
117
  has_many :variant_images, -> { order(:position) }, source: :images, through: :variants_including_master
112
118
  has_many :variant_images_without_master, -> { order(:position) }, source: :images, through: :variants
@@ -203,17 +209,6 @@ module Spree
203
209
  joins(:stock_items).where("#{Spree::Variant.table_name}.track_inventory = ? OR #{Spree::StockItem.table_name}.count_on_hand <= ?", false, 0)
204
210
  }
205
211
 
206
- scope :by_best_selling, lambda { |order_direction = :desc|
207
- left_joins(variants_including_master: { line_items: :order }).
208
- select(
209
- "#{Spree::Product.table_name}.*",
210
- "COUNT(DISTINCT CASE WHEN #{Spree::Order.table_name}.completed_at IS NOT NULL THEN #{Spree::Order.table_name}.id END) AS completed_orders_count",
211
- "COALESCE(SUM(CASE WHEN #{Spree::Order.table_name}.completed_at IS NOT NULL THEN (#{Spree::LineItem.table_name}.price * #{Spree::LineItem.table_name}.quantity) END), 0) AS completed_orders_total"
212
- ).
213
- group("#{Spree::Product.table_name}.id").
214
- order(completed_orders_count: order_direction, completed_orders_total: order_direction)
215
- }
216
-
217
212
  attr_accessor :option_values_hash
218
213
 
219
214
  accepts_nested_attributes_for :product_properties, allow_destroy: true, reject_if: lambda { |pp|
@@ -263,12 +258,12 @@ module Spree
263
258
  event :activate do
264
259
  transition to: :active
265
260
  end
266
- after_transition to: :active, do: [:after_activate, :send_product_activated_webhook]
261
+ after_transition to: :active, do: [:after_activate, :send_product_activated_webhook, :publish_product_activated_event]
267
262
 
268
263
  event :archive do
269
264
  transition to: :archived
270
265
  end
271
- after_transition to: :archived, do: [:after_archive, :send_product_archived_webhook]
266
+ after_transition to: :archived, do: [:after_archive, :send_product_archived_webhook, :publish_product_archived_event]
272
267
 
273
268
  event :draft do
274
269
  transition to: :draft
@@ -316,9 +311,13 @@ module Spree
316
311
  master || build_master
317
312
  end
318
313
 
319
- # the master variant is not a member of the variants array
314
+ # Checks if product has variants (non-master variants)
315
+ # Uses variant_count counter cache for performance
316
+ # @return [Boolean]
320
317
  def has_variants?
321
- @has_variants ||= variants.loaded? ? variants.size.positive? : variants.any?
318
+ return variants.size.positive? if variants.loaded?
319
+
320
+ variant_count.positive?
322
321
  end
323
322
 
324
323
  # Returns default Variant for Product
@@ -330,7 +329,7 @@ module Spree
330
329
  #
331
330
  # @return [Spree::Variant]
332
331
  def default_variant
333
- @default_variant ||= if Spree::Config[:track_inventory_levels] && available_variant = variants.detect(&:purchasable?)
332
+ @default_variant ||= if Spree::Config[:track_inventory_levels] && has_variants? && available_variant = variants.detect(&:purchasable?)
334
333
  available_variant
335
334
  else
336
335
  has_variants? ? variants.first : find_or_build_master
@@ -343,31 +342,52 @@ module Spree
343
342
  @default_variant_id ||= default_variant.id
344
343
  end
345
344
 
346
- # Returns default Image for Product
347
- # @return [Spree::Image]
345
+ # Returns true if any variant (including master) has images.
346
+ # Uses loaded association when available, otherwise falls back to counter cache.
347
+ # @return [Boolean]
348
+ def has_variant_images?
349
+ return variant_images.any? if association(:variant_images).loaded?
350
+
351
+ total_image_count.positive?
352
+ end
353
+
354
+ # Alias for has_variant_images? for consistency with Variant#has_images?
355
+ alias has_images? has_variant_images?
356
+
357
+ # Returns the variant that should be used for displaying images.
358
+ # Priority: master > default_variant > first variant with images
359
+ # @return [Spree::Variant, nil]
360
+ def variant_for_images
361
+ @variant_for_images ||= find_variant_for_images
362
+ end
363
+
364
+ # Returns default Image for Product.
365
+ # @return [Spree::Image, nil]
348
366
  def default_image
349
- @default_image ||= if images.any?
350
- images.first
351
- elsif default_variant.images.any?
352
- default_variant.default_image
353
- elsif variant_images.any?
354
- variant_images.first
355
- end
367
+ variant_for_images&.primary_image
356
368
  end
357
- alias featured_image default_image
358
369
 
359
- # Returns secondary Image for Product
360
- # @return [Spree::Image]
370
+ # Returns secondary Image for Product (for hover effects).
371
+ # @return [Spree::Image, nil]
361
372
  def secondary_image
362
- @secondary_image ||= if images.size > 1
363
- images.second
364
- elsif images.size == 1 && default_variant.images.size.positive?
365
- default_variant.images.first
366
- elsif default_variant.images.size > 1
367
- default_variant.secondary_image
368
- elsif variant_images.size > 1
369
- variant_images.second
370
- end
373
+ variant_for_images&.secondary_image
374
+ end
375
+
376
+ # Alias for default_image for consistency.
377
+ alias primary_image default_image
378
+
379
+ # Returns the image count from the variant used for displaying images.
380
+ # @return [Integer]
381
+ def image_count
382
+ variant_for_images&.image_count || 0
383
+ end
384
+
385
+ # Finds first variant with images using preloaded data when available.
386
+ # @return [Spree::Variant, nil]
387
+ def find_variant_with_images
388
+ return variants.find(&:has_images?) if variants.loaded?
389
+
390
+ variants.joins(:images).first
371
391
  end
372
392
 
373
393
  # Returns the short description for the product
@@ -558,7 +578,7 @@ module Spree
558
578
  if self.class.reflect_on_association(:brand)
559
579
  super
560
580
  else
561
- Spree::Deprecation.warn('Spree::Product#brand is deprecated and will be removed in Spree 6. Please use Spree::Product#brand_taxon instead.')
581
+ Spree::Deprecation.warn('Spree::Product#brand is deprecated and will be removed in Spree 5.5. Please use Spree::Product#brand_taxon instead.')
562
582
  brand_taxon
563
583
  end
564
584
  end
@@ -566,17 +586,19 @@ module Spree
566
586
  # Returns the brand taxon for the product
567
587
  # @return [Spree::Taxon]
568
588
  def brand_taxon
569
- @brand ||= if Spree.use_translations?
570
- taxons.joins(:taxonomy).
571
- join_translation_table(Taxonomy).
572
- find_by(Taxonomy.translation_table_alias => { name: Spree.t(:taxonomy_brands_name) })
573
- else
574
- if taxons.loaded?
575
- taxons.find { |taxon| taxon.taxonomy.name == Spree.t(:taxonomy_brands_name) }
576
- else
577
- taxons.joins(:taxonomy).find_by(Taxonomy.table_name => { name: Spree.t(:taxonomy_brands_name) })
578
- end
579
- end
589
+ @brand_taxon ||= if classification_count.zero?
590
+ nil
591
+ elsif Spree.use_translations?
592
+ taxons.joins(:taxonomy).
593
+ join_translation_table(Taxonomy).
594
+ find_by(Taxonomy.translation_table_alias => { name: Spree.t(:taxonomy_brands_name) })
595
+ else
596
+ if taxons.loaded?
597
+ taxons.find { |taxon| taxon.taxonomy.name == Spree.t(:taxonomy_brands_name) }
598
+ else
599
+ taxons.joins(:taxonomy).find_by(Taxonomy.table_name => { name: Spree.t(:taxonomy_brands_name) })
600
+ end
601
+ end
580
602
  end
581
603
 
582
604
  # Returns the brand name for the product
@@ -593,7 +615,7 @@ module Spree
593
615
  if self.class.reflect_on_association(:category)
594
616
  super
595
617
  else
596
- Spree::Deprecation.warn('Spree::Product#category is deprecated and will be removed in Spree 6. Please use Spree::Product#category_taxon instead.')
618
+ Spree::Deprecation.warn('Spree::Product#category is deprecated and will be removed in Spree 5.5. Please use Spree::Product#category_taxon instead.')
597
619
  category_taxon
598
620
  end
599
621
  end
@@ -601,27 +623,33 @@ module Spree
601
623
  # Returns the category taxon for the product
602
624
  # @return [Spree::Taxon]
603
625
  def category_taxon
604
- @category ||= if Spree.use_translations?
605
- taxons.joins(:taxonomy).
606
- join_translation_table(Taxonomy).
607
- order(depth: :desc).
608
- find_by(Taxonomy.translation_table_alias => { name: Spree.t(:taxonomy_categories_name) })
609
- else
610
- if taxons.loaded?
611
- taxons.find { |taxon| taxon.taxonomy.name == Spree.t(:taxonomy_categories_name) }
612
- else
613
- taxons.joins(:taxonomy).order(depth: :desc).find_by(Taxonomy.table_name => { name: Spree.t(:taxonomy_categories_name) })
614
- end
615
- end
626
+ @category_taxon ||= if classification_count.zero?
627
+ nil
628
+ elsif Spree.use_translations?
629
+ taxons.joins(:taxonomy).
630
+ join_translation_table(Taxonomy).
631
+ order(depth: :desc).
632
+ find_by(Taxonomy.translation_table_alias => { name: Spree.t(:taxonomy_categories_name) })
633
+ else
634
+ if taxons.loaded?
635
+ taxons.find { |taxon| taxon.taxonomy.name == Spree.t(:taxonomy_categories_name) }
636
+ else
637
+ taxons.joins(:taxonomy).order(depth: :desc).find_by(Taxonomy.table_name => { name: Spree.t(:taxonomy_categories_name) })
638
+ end
639
+ end
616
640
  end
617
641
 
618
642
  def main_taxon
619
- category_taxon || taxons.first
643
+ return if classification_count.zero?
644
+
645
+ @main_taxon ||= category_taxon || taxons.first
620
646
  end
621
647
 
622
648
  def taxons_for_store(store)
649
+ return if classification_count.zero?
650
+
623
651
  Rails.cache.fetch("#{cache_key_with_version}/taxons-per-store/#{store.id}") do
624
- taxons.for_store(store)
652
+ taxons.loaded? ? taxons.find_all { |taxon| taxon.taxonomy.store_id == store.id } : taxons.for_store(store)
625
653
  end
626
654
  end
627
655
 
@@ -674,7 +702,8 @@ module Spree
674
702
 
675
703
  if has_variants?
676
704
  variants_including_master.each_with_index do |variant, index|
677
- csv_lines << Spree::CSV::ProductVariantPresenter.new(self, variant, index, properties_for_csv, taxons_for_csv, store, metafields_for_csv).call
705
+ csv_lines << Spree::CSV::ProductVariantPresenter.new(self, variant, index, properties_for_csv, taxons_for_csv, store,
706
+ metafields_for_csv).call
678
707
  end
679
708
  else
680
709
  csv_lines << Spree::CSV::ProductVariantPresenter.new(self, master, 0, properties_for_csv, taxons_for_csv, store, metafields_for_csv).call
@@ -683,13 +712,17 @@ module Spree
683
712
  csv_lines
684
713
  end
685
714
 
686
- def page_builder_url
687
- return unless Spree::Core::Engine.routes.url_helpers.respond_to?(:product_path)
715
+ private
688
716
 
689
- Spree::Core::Engine.routes.url_helpers.product_path(self)
690
- end
717
+ # Determines which variant should be used for displaying images.
718
+ # Priority: master > default_variant > first variant with images
719
+ def find_variant_for_images
720
+ return master if master.has_images?
721
+ return default_variant if has_variants? && default_variant.has_images?
722
+ return find_variant_with_images if has_variant_images?
691
723
 
692
- private
724
+ nil
725
+ end
693
726
 
694
727
  def add_associations_from_prototype
695
728
  if prototype_id && prototype = Spree::Prototype.find_by(id: prototype_id)
@@ -727,7 +760,7 @@ module Spree
727
760
  end
728
761
 
729
762
  def default_variant_cache_key
730
- Spree::Deprecation.warn('Spree::Product#default_variant_cache_key is deprecated and will be removed in Spree 6. Please remove any occurrences of it.')
763
+ Spree::Deprecation.warn('Spree::Product#default_variant_cache_key is deprecated and will be removed in Spree 5.5. Please remove any occurrences of it.')
731
764
 
732
765
  "spree/default-variant/#{cache_key_with_version}/#{Spree::Config[:track_inventory_levels]}"
733
766
  end
@@ -855,5 +888,13 @@ module Spree
855
888
  def after_draft
856
889
  # Implement your logic here
857
890
  end
891
+
892
+ def publish_product_activated_event
893
+ publish_event('product.activated')
894
+ end
895
+
896
+ def publish_product_archived_event
897
+ publish_event('product.archived')
898
+ end
858
899
  end
859
900
  end
@@ -12,10 +12,10 @@ module Spree
12
12
  end
13
13
 
14
14
  self::Translation.class_eval do
15
- auto_strip_attributes :value
15
+ normalizes :value, with: ->(value) { value&.to_s&.squish&.presence }
16
16
  end
17
17
 
18
- auto_strip_attributes :value
18
+ normalizes :value, with: ->(value) { value&.to_s&.squish&.presence }
19
19
 
20
20
  acts_as_list scope: :product
21
21
 
@@ -0,0 +1,22 @@
1
+ module Spree
2
+ class Promotion
3
+ module Rules
4
+ class CustomerGroup < PromotionRule
5
+ preference :customer_group_ids, :array, default: []
6
+
7
+ def applicable?(promotable)
8
+ promotable.is_a?(Spree::Order)
9
+ end
10
+
11
+ def eligible?(order, _options = {})
12
+ return false unless order.user_id.present?
13
+ return false if preferred_customer_group_ids.empty?
14
+
15
+ user_customer_group_ids = Spree::CustomerGroupUser.where(user_id: order.user_id).pluck(:customer_group_id).map(&:to_s)
16
+
17
+ (preferred_customer_group_ids.map(&:to_s) & user_customer_group_ids).any?
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -34,7 +34,7 @@ module Spree
34
34
 
35
35
  def user_ids_string
36
36
  ActiveSupport::Deprecation.warn(
37
- 'Spree::Promotion::Rules::User#user_ids_string is deprecated and will be removed in Spree 6.0. ' \
37
+ 'Spree::Promotion::Rules::User#user_ids_string is deprecated and will be removed in Spree 5.5. ' \
38
38
  'Please use `user_ids` instead.'
39
39
  )
40
40
  user_ids.join(',')
@@ -42,7 +42,7 @@ module Spree
42
42
 
43
43
  def user_ids_string=(s)
44
44
  ActiveSupport::Deprecation.warn(
45
- 'Spree::Promotion::Rules::User#user_ids_string= is deprecated and will be removed in Spree 6.0. ' \
45
+ 'Spree::Promotion::Rules::User#user_ids_string= is deprecated and will be removed in Spree 5.5. ' \
46
46
  'Please use `user_ids=` instead.'
47
47
  )
48
48
  self.user_ids = s
@@ -3,13 +3,12 @@ module Spree
3
3
  include Spree::MultiStoreResource
4
4
  include Spree::Metafields
5
5
  include Spree::Metadata
6
- if defined?(Spree::Webhooks::HasWebhooks)
7
- include Spree::Webhooks::HasWebhooks
8
- end
9
6
  if defined?(Spree::Security::Promotions)
10
7
  include Spree::Security::Promotions
11
8
  end
12
9
 
10
+ publishes_lifecycle_events
11
+
13
12
  MATCH_POLICIES = %w(all any)
14
13
  UNACTIVATABLE_ORDER_STATES = ['complete', 'awaiting_return', 'returned']
15
14
 
@@ -18,7 +17,7 @@ module Spree
18
17
  #
19
18
  # Magic methods
20
19
  #
21
- auto_strip_attributes :code, :path, :name
20
+ normalizes :code, :path, :name, with: ->(value) { value&.to_s&.squish&.presence }
22
21
 
23
22
  #
24
23
  # Enums
@@ -177,8 +177,8 @@ module Spree
177
177
 
178
178
  # Check for applied adjustments.
179
179
  discount = order.all_adjustments.promotion.eligible.detect do |p|
180
- p.source.promotion.code.try(:downcase) == coupon_code ||
181
- p.source.promotion.coupon_codes.unused.where(code: coupon_code).exists?
180
+ p.cached_source.promotion.code.try(:downcase) == coupon_code ||
181
+ Spree::CouponCode.unused.where(promotion_id: p.cached_source.promotion_id, code: coupon_code).exists?
182
182
  end
183
183
 
184
184
  # Check for applied line items.
@@ -209,7 +209,7 @@ module Spree
209
209
  end
210
210
 
211
211
  def handle_coupon_code(discount, coupon_code)
212
- discount.source.promotion.coupon_codes.unused.find_by(code: coupon_code)&.apply_order!(order)
212
+ Spree::CouponCode.unused.find_by(promotion_id: discount.cached_source.promotion_id, code: coupon_code)&.apply_order!(order)
213
213
  end
214
214
 
215
215
  def load_gift_card_code
@@ -6,15 +6,12 @@ module Spree
6
6
  include Spree::UniqueName
7
7
  include Spree::DisplayOn
8
8
  include Spree::TranslatableResource
9
- if defined?(Spree::Webhooks::HasWebhooks)
10
- include Spree::Webhooks::HasWebhooks
11
- end
12
9
 
13
10
  TRANSLATABLE_FIELDS = %i[presentation].freeze
14
11
  translates(*TRANSLATABLE_FIELDS, column_fallback: !Spree.always_use_translations?)
15
12
 
16
13
  self::Translation.class_eval do
17
- auto_strip_attributes :presentation
14
+ normalizes :presentation, with: ->(value) { value&.to_s&.squish&.presence }
18
15
  end
19
16
 
20
17
  acts_as_list
@@ -46,7 +43,7 @@ module Spree
46
43
  with_uniq_values_cache_key(product_properties_scope) do
47
44
  properties = product_properties
48
45
  properties = properties.where(id: product_properties_scope) if product_properties_scope.present?
49
- properties.where.not(value: [nil, '']).pluck(:filter_param, :value).uniq
46
+ properties.where('value IS NOT NULL AND value != ?', '').pluck(:filter_param, :value).uniq
50
47
  end
51
48
  end
52
49
 
@@ -83,7 +80,7 @@ module Spree
83
80
  def ensure_product_properties_have_filter_params
84
81
  return unless filterable?
85
82
 
86
- product_properties.where(filter_param: [nil, '']).where.not(value: [nil, '']).find_each(&:save)
83
+ product_properties.where(filter_param: [nil, '']).where('value IS NOT NULL AND value != ?', '').find_each(&:save)
87
84
  end
88
85
  end
89
86
  end
@@ -1,9 +1,6 @@
1
1
  module Spree
2
2
  class Prototype < Spree.base_class
3
3
  include Spree::Metadata
4
- if defined?(Spree::Webhooks::HasWebhooks)
5
- include Spree::Webhooks::HasWebhooks
6
- end
7
4
 
8
5
  has_many :property_prototypes, class_name: 'Spree::PropertyPrototype'
9
6
  has_many :properties, through: :property_prototypes, class_name: 'Spree::Property'
@@ -2,13 +2,12 @@ module Spree
2
2
  class Refund < Spree.base_class
3
3
  include Spree::Metafields
4
4
  include Spree::Metadata
5
- if defined?(Spree::Webhooks::HasWebhooks)
6
- include Spree::Webhooks::HasWebhooks
7
- end
8
5
  if defined?(Spree::Security::Refunds)
9
6
  include Spree::Security::Refunds
10
7
  end
11
8
 
9
+ publishes_lifecycle_events
10
+
12
11
  with_options inverse_of: :refunds do
13
12
  belongs_to :payment
14
13
  belongs_to :reimbursement, optional: true
@@ -2,9 +2,6 @@ module Spree
2
2
  class Reimbursement < Spree.base_class
3
3
  include Spree::Core::NumberGenerator.new(prefix: 'RI', length: 9)
4
4
  include Spree::NumberIdentifier
5
- if defined?(Spree::Webhooks::HasWebhooks)
6
- include Spree::Webhooks::HasWebhooks
7
- end
8
5
  include Spree::Reimbursement::Emails
9
6
 
10
7
  class IncompleteReimbursementError < StandardError; end
@@ -76,6 +73,7 @@ module Spree
76
73
  event :reimbursed do
77
74
  transition to: :reimbursed, from: [:pending, :errored]
78
75
  end
76
+ after_transition to: :reimbursed, do: :publish_reimbursement_reimbursed_event
79
77
  end
80
78
 
81
79
  class << self
@@ -138,6 +136,10 @@ module Spree
138
136
 
139
137
  private
140
138
 
139
+ def publish_reimbursement_reimbursed_event
140
+ publish_event('reimbursement.reimbursed')
141
+ end
142
+
141
143
  def validate_return_items_belong_to_same_order
142
144
  if return_items.any? { |ri| ri.inventory_unit.order_id != order_id }
143
145
  errors.add(:base, :return_items_order_id_does_not_match)
@@ -3,6 +3,12 @@ module Spree
3
3
  include Spree::SingleStoreResource
4
4
  include Spree::VendorConcern if defined?(Spree::VendorConcern)
5
5
 
6
+ publishes_lifecycle_events
7
+
8
+ # Set event prefix for all Report subclasses
9
+ # This ensures Spree::Reports::SalesTotal publishes 'report.create' not 'sales_total.create'
10
+ self.event_prefix = 'report'
11
+
6
12
  #
7
13
  # Associations
8
14
  #
@@ -13,7 +19,7 @@ module Spree
13
19
  # Callbacks
14
20
  #
15
21
  after_initialize :set_default_values
16
- after_commit :generate_async, on: :create
22
+ # NOTE: generate_async is now handled by Spree::ReportSubscriber listening to 'report.create' event
17
23
 
18
24
  #
19
25
  # Validations
@@ -17,7 +17,7 @@ module Spree
17
17
  currency: currency
18
18
  },
19
19
  spree_orders: {
20
- completed_at: (date_from.to_time.beginning_of_day)..(date_to.to_time.end_of_day)
20
+ completed_at: date_from..date_to
21
21
  }
22
22
  )
23
23
 
@@ -5,7 +5,7 @@ module Spree
5
5
  scope = store.line_items.where(
6
6
  order: Spree::Order.complete.where(
7
7
  currency: currency,
8
- completed_at: (date_from.to_time.beginning_of_day)..(date_to.to_time.end_of_day)
8
+ completed_at: date_from..date_to
9
9
  )
10
10
  ).includes(:order, shipments: :inventory_units, variant: :product)
11
11
 
@@ -2,9 +2,8 @@ module Spree
2
2
  class ReturnAuthorization < Spree.base_class
3
3
  include Spree::Core::NumberGenerator.new(prefix: 'RA', length: 9)
4
4
  include Spree::NumberIdentifier
5
- if defined?(Spree::Webhooks::HasWebhooks)
6
- include Spree::Webhooks::HasWebhooks
7
- end
5
+
6
+ publishes_lifecycle_events
8
7
 
9
8
  belongs_to :order, class_name: 'Spree::Order', inverse_of: :return_authorizations
10
9
 
@@ -34,6 +33,7 @@ module Spree
34
33
 
35
34
  state_machine initial: :authorized do
36
35
  before_transition to: :canceled, do: :cancel_return_items
36
+ after_transition to: :canceled, do: :publish_return_authorization_canceled_event
37
37
 
38
38
  event :cancel do
39
39
  transition to: :canceled, from: :authorized, if: ->(return_authorization) { return_authorization.can_cancel_return_items? }
@@ -79,6 +79,10 @@ module Spree
79
79
  return_items.each { |item| item.cancel! if item.can_cancel? }
80
80
  end
81
81
 
82
+ def publish_return_authorization_canceled_event
83
+ publish_event('return_authorization.canceled')
84
+ end
85
+
82
86
  def generate_expedited_exchange_reimbursements
83
87
  return unless Spree::Config[:expedited_exchanges]
84
88
 
@@ -2,10 +2,6 @@ module Spree
2
2
  class ReturnItem < Spree.base_class
3
3
  COMPLETED_RECEPTION_STATUSES = %w(received given_to_customer)
4
4
 
5
- if defined?(Spree::Webhooks::HasWebhooks)
6
- include Spree::Webhooks::HasWebhooks
7
- end
8
-
9
5
  class_attribute :return_eligibility_validator
10
6
  self.return_eligibility_validator = ReturnItem::EligibilityValidator::Default
11
7
 
@@ -85,6 +81,7 @@ module Spree
85
81
  state_machine :reception_status, initial: :awaiting do
86
82
  after_transition to: :received, do: :attempt_accept
87
83
  after_transition to: :received, do: :process_inventory_unit!
84
+ after_transition to: :received, do: :publish_return_item_received_event
88
85
 
89
86
  event :receive do
90
87
  transition to: :received, from: :awaiting
@@ -93,10 +90,12 @@ module Spree
93
90
  event :cancel do
94
91
  transition to: :cancelled, from: :awaiting
95
92
  end
93
+ after_transition to: :cancelled, do: :publish_return_item_canceled_event
96
94
 
97
95
  event :give do
98
96
  transition to: :given_to_customer, from: :awaiting
99
97
  end
98
+ after_transition to: :given_to_customer, do: :publish_return_item_given_event
100
99
  end
101
100
 
102
101
  extend DisplayMoney
@@ -270,5 +269,17 @@ module Spree
270
269
  def should_restock?
271
270
  resellable? && variant.should_track_inventory? && stock_item && Spree::Config[:restock_inventory]
272
271
  end
272
+
273
+ def publish_return_item_received_event
274
+ publish_event('return_item.received')
275
+ end
276
+
277
+ def publish_return_item_canceled_event
278
+ publish_event('return_item.canceled')
279
+ end
280
+
281
+ def publish_return_item_given_event
282
+ publish_event('return_item.given')
283
+ end
273
284
  end
274
285
  end