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
@@ -65,7 +65,11 @@ module Spree
65
65
  #
66
66
  # @return [BigDecimal] The total amount of store credits applied to the order.
67
67
  def total_applied_store_credit
68
- payments.store_credits.valid.sum(:amount)
68
+ if payments.loaded?
69
+ payments.find_all(&:store_credit?).find_all(&:valid?).sum(&:amount) || BigDecimal::ZERO
70
+ else
71
+ payments.store_credits.valid.sum(:amount)
72
+ end
69
73
  end
70
74
 
71
75
  # Returns true if the order is using store credit.
@@ -28,6 +28,8 @@ module Spree
28
28
  include Spree::NumberIdentifier
29
29
  include Spree::NumberAsParam
30
30
  include Spree::SingleStoreResource
31
+
32
+ publishes_lifecycle_events
31
33
  include Spree::MemoizedData
32
34
  include Spree::Metafields
33
35
  include Spree::Metadata
@@ -40,6 +42,7 @@ module Spree
40
42
  end
41
43
 
42
44
  has_secure_token :token, length: 35
45
+ has_rich_text :internal_note
43
46
 
44
47
  MEMOIZED_METHODS = %w(tax_zone)
45
48
 
@@ -202,8 +205,8 @@ module Spree
202
205
  joins(:refunds).group(:id).having("sum(#{Spree::Refund.table_name}.amount) = #{Spree::Order.table_name}.total")
203
206
  }
204
207
  scope :partially_refunded, lambda {
205
- joins(:refunds).group(:id).having("sum(#{Spree::Refund.table_name}.amount) < #{Spree::Order.table_name}.total")
206
- }
208
+ joins(:refunds).group(:id).having("sum(#{Spree::Refund.table_name}.amount) < #{Spree::Order.table_name}.total")
209
+ }
207
210
  scope :with_deleted_bill_address, -> { joins(:bill_address).where.not(Address.table_name => { deleted_at: nil }) }
208
211
  scope :with_deleted_ship_address, -> { joins(:ship_address).where.not(Address.table_name => { deleted_at: nil }) }
209
212
 
@@ -268,15 +271,26 @@ module Spree
268
271
  completed_at.present?
269
272
  end
270
273
 
274
+ # Checks if the order is fully refunded
275
+ # @return [Boolean]
271
276
  def order_refunded?
272
- (payment_state.in?(%w[void failed]) && refunds.sum(:amount).positive?) ||
273
- refunds.sum(:amount) == total_minus_store_credits - additional_tax_total.abs
277
+ return false if item_count.zero?
278
+
279
+ (payment_state.in?(%w[void failed]) && refunds_total.positive?) ||
280
+ refunds_total == total_minus_store_credits - additional_tax_total.abs
274
281
  end
275
282
 
283
+ def refunds_total
284
+ refunds.loaded? ? refunds.sum(&:amount) : refunds.sum(:amount)
285
+ end
286
+
287
+ # Checks if the order is partially refunded
288
+ # @return [Boolean]
276
289
  def partially_refunded?
277
- return false if refunds.empty? || payment_state.in?(%w[void failed])
290
+ return false if item_count.zero?
291
+ return false if payment_state.in?(%w[void failed]) || refunds.empty?
278
292
 
279
- refunds.sum(:amount) < total_minus_store_credits - additional_tax_total.abs
293
+ refunds_total < total_minus_store_credits - additional_tax_total.abs
280
294
  end
281
295
 
282
296
  # Indicates whether or not the user is allowed to proceed to checkout.
@@ -391,6 +405,9 @@ module Spree
391
405
  ActiveRecord::Base.connected_to(role: :writing) do
392
406
  self.class.unscoped.where(id: self).update_all(changes)
393
407
  end
408
+
409
+ # Manually publish update event since update_all bypasses callbacks
410
+ publish_event('order.updated') if changes.present?
394
411
  end
395
412
 
396
413
  def disassociate_user!
@@ -520,12 +537,11 @@ module Spree
520
537
 
521
538
  touch :completed_at
522
539
 
523
- deliver_order_confirmation_email unless confirmation_delivered?
524
- deliver_store_owner_order_notification_email if deliver_store_owner_order_notification_email?
525
-
526
540
  send_order_placed_webhook
527
541
 
528
542
  consider_risk
543
+
544
+ publish_order_completed_event
529
545
  end
530
546
 
531
547
  def fulfill!
@@ -540,10 +556,7 @@ module Spree
540
556
  end
541
557
 
542
558
  def available_payment_methods(store = nil)
543
- Spree::Deprecation.warn('`Order#available_payment_methods` is deprecated and will be removed in Spree 6. Use `collect_frontend_payment_methods` instead.')
544
- if store.present?
545
- Spree::Deprecation.warn('The `store` parameter is deprecated and will be removed in Spree 5. Order is already associated with Store')
546
- end
559
+ Spree::Deprecation.warn('`Order#available_payment_methods` is deprecated and will be removed in Spree 5.5. Use `collect_frontend_payment_methods` instead.')
547
560
 
548
561
  @available_payment_methods ||= collect_payment_methods(store)
549
562
  end
@@ -691,6 +704,10 @@ module Spree
691
704
  if shipments.any? && !completed?
692
705
  shipments.destroy_all
693
706
  update_column(:shipment_total, 0)
707
+
708
+ # Manually publish update event since update_column bypasses callbacks
709
+ publish_event('order.updated')
710
+
694
711
  restart_checkout_flow
695
712
  end
696
713
  end
@@ -700,6 +717,10 @@ module Spree
700
717
  state: 'cart',
701
718
  updated_at: Time.current
702
719
  )
720
+
721
+ # Manually publish update event since update_columns bypasses callbacks
722
+ publish_event('order.updated')
723
+
703
724
  next! unless line_items.empty?
704
725
  end
705
726
 
@@ -733,24 +754,28 @@ module Spree
733
754
 
734
755
  def canceled_by(user, canceled_at = nil)
735
756
  canceled_at ||= Time.current
757
+ changes = { canceler_id: user.id, canceled_at: canceled_at }
736
758
 
737
759
  transaction do
738
- update_columns(
739
- canceler_id: user.id,
740
- canceled_at: canceled_at
741
- )
760
+ update_columns(changes)
742
761
  cancel!
743
762
  end
763
+
764
+ # Manually publish update event since update_columns bypasses callbacks
765
+ publish_event('order.canceled')
744
766
  end
745
767
 
746
768
  def approved_by(user)
769
+ approved_at = Time.current
770
+ changes = { approver_id: user.id, approved_at: approved_at }
771
+
747
772
  transaction do
748
773
  approve!
749
- update_columns(
750
- approver_id: user.id,
751
- approved_at: Time.current
752
- )
774
+ update_columns(changes)
753
775
  end
776
+
777
+ # Manually publish update event since update_columns bypasses callbacks
778
+ publish_event('order.approved')
754
779
  end
755
780
 
756
781
  def approved?
@@ -776,10 +801,16 @@ module Spree
776
801
 
777
802
  def considered_risky!
778
803
  update_column(:considered_risky, true)
804
+
805
+ # Manually publish update event since update_column bypasses callbacks
806
+ publish_event('order.updated')
779
807
  end
780
808
 
781
809
  def approve!
782
810
  update_column(:considered_risky, false)
811
+
812
+ # Manually publish update event since update_column bypasses callbacks
813
+ publish_event('order.approved')
783
814
  end
784
815
 
785
816
  def tax_total
@@ -817,14 +848,22 @@ module Spree
817
848
  Spree::CouponCode.find_by(order: self, promotion: promotions).try(:code) || promotions.pluck(:code).compact.first
818
849
  end
819
850
 
851
+ # Returns the valid promotions for the order
852
+ # @return [Array<Spree::OrderPromotion>]
820
853
  def valid_promotions
821
- order_promotions.where(promotion_id: valid_promotion_ids).uniq(&:promotion_id)
854
+ order_promotions.includes(:promotion).where(promotion_id: valid_promotion_ids).uniq(&:promotion_id)
822
855
  end
823
856
 
857
+ # Returns the IDs of the valid promotions for the order
858
+ # @return [Array<Integer>]
824
859
  def valid_promotion_ids
825
- all_adjustments.eligible.nonzero.promotion.map { |a| a.source.promotion_id }.uniq
860
+ all_adjustments.eligible.nonzero.promotion.promotion.eligible.nonzero.promotion.
861
+ joins("INNER JOIN #{Spree::PromotionAction.table_name} ON #{Spree::PromotionAction.table_name}.id = #{Spree::Adjustment.table_name}.source_id").
862
+ pluck("#{Spree::PromotionAction.table_name}.promotion_id").compact.uniq
826
863
  end
827
864
 
865
+ # Returns the valid coupon promotions for the order
866
+ # @return [Array<Spree::Promotion>]
828
867
  def valid_coupon_promotions
829
868
  promotions.
830
869
  where(id: valid_promotion_ids).
@@ -832,7 +871,7 @@ module Spree
832
871
  end
833
872
 
834
873
  # Returns item and whole order discount amount for Order
835
- # without Shipment disccounts (eg. Free Shipping)
874
+ # without Shipment discounts (eg. Free Shipping)
836
875
  # @return [BigDecimal]
837
876
  def cart_promo_total
838
877
  all_adjustments.eligible.nonzero.promotion.
@@ -897,7 +936,7 @@ module Spree
897
936
  errors.add(:base, Spree.t(:items_cannot_be_shipped))
898
937
  end
899
938
 
900
- return false
939
+ false
901
940
  end
902
941
  end
903
942
 
@@ -917,12 +956,14 @@ module Spree
917
956
  send_cancel_email
918
957
  update_with_updater!
919
958
  send_order_canceled_webhook
959
+ publish_order_canceled_event
920
960
  end
921
961
 
922
962
  def after_resume
923
963
  shipments.each(&:resume!)
924
964
  consider_risk
925
965
  send_order_resumed_webhook
966
+ publish_order_resumed_event
926
967
  end
927
968
 
928
969
  def use_billing?
@@ -937,12 +978,8 @@ module Spree
937
978
  self.currency ||= store&.default_currency
938
979
  end
939
980
 
940
- def collect_payment_methods(store = nil)
941
- Spree::Deprecation.warn('`Order#collect_payment_methods` is deprecated and will be removed in Spree 6. Use `collect_frontend_payment_methods` instead.')
942
- if store.present?
943
- Spree::Deprecation.warn('The `store` parameter is deprecated and will be removed in Spree 5. Order is already associated with Store')
944
- end
945
- store ||= self.store
981
+ def collect_payment_methods
982
+ Spree::Deprecation.warn('`Order#collect_payment_methods` is deprecated and will be removed in Spree 5.5. Use `collect_frontend_payment_methods` instead.')
946
983
 
947
984
  store.payment_methods.available_on_front_end.select { |pm| pm.available_for_order?(self) }
948
985
  end
@@ -960,5 +997,17 @@ module Spree
960
997
  Spree.checkout_add_store_credit_service.call(order: self)
961
998
  end
962
999
  end
1000
+
1001
+ def publish_order_completed_event
1002
+ publish_event('order.completed')
1003
+ end
1004
+
1005
+ def publish_order_canceled_event
1006
+ publish_event('order.canceled')
1007
+ end
1008
+
1009
+ def publish_order_resumed_event
1010
+ publish_event('order.resumed')
1011
+ end
963
1012
  end
964
1013
  end
@@ -79,15 +79,34 @@ module Spree
79
79
 
80
80
  def update_adjustment_total
81
81
  recalculate_adjustments
82
- order.adjustment_total = line_items.sum(:adjustment_total) +
83
- shipments.sum(:adjustment_total) +
84
- adjustments.eligible.sum(:amount)
85
- order.included_tax_total = line_items.sum(:included_tax_total) + shipments.sum(:included_tax_total)
86
- order.additional_tax_total = line_items.sum(:additional_tax_total) + shipments.sum(:additional_tax_total)
87
-
88
- order.promo_total = line_items.sum(:promo_total) +
89
- shipments.sum(:promo_total) +
90
- adjustments.promotion.eligible.sum(:amount)
82
+
83
+ # Fetch all line item totals in a single query
84
+ # Use reorder(nil) to remove default ordering which conflicts with aggregates in PostgreSQL
85
+ line_item_totals = line_items.reorder(nil).pick(
86
+ Arel.sql('COALESCE(SUM(adjustment_total), 0)'),
87
+ Arel.sql('COALESCE(SUM(included_tax_total), 0)'),
88
+ Arel.sql('COALESCE(SUM(additional_tax_total), 0)'),
89
+ Arel.sql('COALESCE(SUM(promo_total), 0)')
90
+ ) || [0, 0, 0, 0]
91
+
92
+ # Fetch all shipment totals in a single query
93
+ shipment_totals = shipments.reorder(nil).pick(
94
+ Arel.sql('COALESCE(SUM(adjustment_total), 0)'),
95
+ Arel.sql('COALESCE(SUM(included_tax_total), 0)'),
96
+ Arel.sql('COALESCE(SUM(additional_tax_total), 0)'),
97
+ Arel.sql('COALESCE(SUM(promo_total), 0)')
98
+ ) || [0, 0, 0, 0]
99
+
100
+ # Fetch order-level adjustment totals in a single query
101
+ order_adjustment_totals = adjustments.eligible.reorder(nil).pick(
102
+ Arel.sql('COALESCE(SUM(amount), 0)'),
103
+ Arel.sql("COALESCE(SUM(CASE WHEN source_type = 'Spree::PromotionAction' THEN amount ELSE 0 END), 0)")
104
+ ) || [0, 0]
105
+
106
+ order.adjustment_total = line_item_totals[0] + shipment_totals[0] + order_adjustment_totals[0]
107
+ order.included_tax_total = line_item_totals[1] + shipment_totals[1]
108
+ order.additional_tax_total = line_item_totals[2] + shipment_totals[2]
109
+ order.promo_total = line_item_totals[3] + shipment_totals[3] + order_adjustment_totals[1]
91
110
 
92
111
  update_order_total
93
112
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ class Payment < Spree.base_class
5
+ # Publishes custom payment events beyond basic lifecycle events.
6
+ #
7
+ # Events:
8
+ # - payment.paid: Payment was completed
9
+ # - order.paid: Order is fully paid (no outstanding balance)
10
+ #
11
+ module CustomEvents
12
+ extend ActiveSupport::Concern
13
+
14
+ included do
15
+ after_commit :publish_payment_paid_event, on: :update, if: :should_publish_paid_event?
16
+ end
17
+
18
+ private
19
+
20
+ def should_publish_paid_event?
21
+ return false unless Spree::Events.enabled?
22
+ return false unless state_previously_changed?
23
+
24
+ state_previous_change&.last == 'completed'
25
+ end
26
+
27
+ def publish_payment_paid_event
28
+ publish_event('payment.paid')
29
+ publish_order_paid_event if order.paid?
30
+ end
31
+
32
+ def publish_order_paid_event
33
+ order.publish_event('order.paid')
34
+ end
35
+ end
36
+ end
37
+ end
@@ -15,6 +15,9 @@ module Spree
15
15
 
16
16
  include Spree::Payment::Processing
17
17
  include Spree::Payment::Webhooks
18
+ include Spree::Payment::CustomEvents
19
+
20
+ publishes_lifecycle_events
18
21
 
19
22
  NON_RISKY_AVS_CODES = ['B', 'D', 'H', 'J', 'M', 'Q', 'T', 'V', 'X', 'Y'].freeze
20
23
  RISKY_AVS_CODES = ['A', 'C', 'E', 'F', 'G', 'I', 'K', 'L', 'N', 'O', 'P', 'R', 'S', 'U', 'W', 'Z'].freeze
@@ -114,11 +117,11 @@ module Spree
114
117
  event :complete do
115
118
  transition from: [:processing, :pending, :checkout], to: :completed
116
119
  end
117
- after_transition to: :completed, do: [:after_completed, :send_payment_completed_webhook]
120
+ after_transition to: :completed, do: [:after_completed, :send_payment_completed_webhook, :publish_payment_completed_event]
118
121
  event :void do
119
122
  transition from: [:pending, :processing, :completed, :checkout], to: :void
120
123
  end
121
- after_transition to: :void, do: [:after_void, :send_payment_voided_webhook]
124
+ after_transition to: :void, do: [:after_void, :send_payment_voided_webhook, :publish_payment_voided_event]
122
125
  # when the card brand isn't supported
123
126
  event :invalidate do
124
127
  transition from: [:checkout], to: :invalid
@@ -286,11 +289,17 @@ module Spree
286
289
  end
287
290
 
288
291
  def after_void
289
- # Implement your logic here
290
292
  end
291
293
 
292
294
  def after_completed
293
- # Implement your logic here
295
+ end
296
+
297
+ def publish_payment_completed_event
298
+ publish_event('payment.completed')
299
+ end
300
+
301
+ def publish_payment_voided_event
302
+ publish_event('payment.voided')
294
303
  end
295
304
 
296
305
  def has_invalid_state?
@@ -1,9 +1,5 @@
1
1
  module Spree
2
2
  class PaymentCaptureEvent < Spree.base_class
3
- if defined?(Spree::Webhooks::HasWebhooks)
4
- include Spree::Webhooks::HasWebhooks
5
- end
6
-
7
3
  belongs_to :payment, class_name: 'Spree::Payment'
8
4
 
9
5
  def display_amount
@@ -18,7 +18,7 @@ module Spree
18
18
  after_initialize :set_name, if: :new_record?
19
19
 
20
20
  validates :name, presence: true
21
- auto_strip_attributes :name
21
+ normalizes :name, with: ->(value) { value&.to_s&.squish&.presence }
22
22
 
23
23
  has_many :store_payment_methods, class_name: 'Spree::StorePaymentMethod'
24
24
  has_many :stores, class_name: 'Spree::Store', through: :store_payment_methods
@@ -26,9 +26,9 @@ module Spree
26
26
 
27
27
  # Content pages
28
28
  can :read, Spree::Policy
29
- can :read, Spree::Page
30
- can :read, Spree::Post
31
- can :read, Spree::PostCategory
29
+ can :read, Spree::Page if defined?(Spree::Page)
30
+ can :read, Spree::Post if defined?(Spree::Post)
31
+ can :read, Spree::PostCategory if defined?(Spree::PostCategory)
32
32
 
33
33
  # Order management for the user's own orders
34
34
  can :create, Spree::Order
@@ -2,7 +2,6 @@ module Spree
2
2
  class Policy < Spree.base_class
3
3
  extend FriendlyId
4
4
  include Spree::TranslatableResource
5
- include Spree::Linkable
6
5
 
7
6
  UNIQUENESS_SCOPE = %i[owner_id owner_type].freeze
8
7
 
@@ -37,7 +36,7 @@ module Spree
37
36
  #
38
37
  # Scopes
39
38
  #
40
- scope :with_body, -> { joins(:rich_text_body).distinct }
39
+ scope :with_body, -> { joins(:rich_text_body).distinct }
41
40
  scope :without_body, -> { where.missing(:rich_text_body) }
42
41
  scope :with_matching_name, ->(name_to_match) do
43
42
  value = name_to_match.to_s.strip.downcase
@@ -62,12 +61,6 @@ module Spree
62
61
  store.policies.or(where.not(owner_type: 'Spree::Store'))
63
62
  end
64
63
 
65
- def page_builder_url
66
- return unless Spree::Core::Engine.routes.url_helpers.respond_to?(:policy_path)
67
-
68
- Spree::Core::Engine.routes.url_helpers.policy_path(self)
69
- end
70
-
71
64
  def with_body?
72
65
  body.present?
73
66
  end
@@ -1,10 +1,11 @@
1
1
  module Spree
2
2
  class Post < Spree.base_class
3
3
  include Spree::SingleStoreResource
4
- include Spree::Linkable
5
4
  include Spree::Metafields
6
5
  extend FriendlyId
7
6
 
7
+ publishes_lifecycle_events
8
+
8
9
  friendly_id :slug_candidates, use: %i[slugged scoped history], scope: %i[store_id deleted?]
9
10
  acts_as_paranoid
10
11
  acts_as_taggable_on :tags
@@ -76,12 +77,6 @@ module Spree
76
77
  published_at.present?
77
78
  end
78
79
 
79
- def page_builder_url
80
- return unless Spree::Core::Engine.routes.url_helpers.respond_to?(:post_path)
81
-
82
- Spree::Core::Engine.routes.url_helpers.post_path(self)
83
- end
84
-
85
80
  def publish(date = nil)
86
81
  update(published_at: date || Time.current)
87
82
  end
@@ -4,6 +4,8 @@ module Spree
4
4
  include Spree::Metafields
5
5
  extend FriendlyId
6
6
 
7
+ publishes_lifecycle_events
8
+
7
9
  friendly_id :slug_candidates, use: %i[slugged scoped history], scope: %i[store_id]
8
10
 
9
11
  #
@@ -1,15 +1,15 @@
1
1
  module Spree
2
2
  class Price < Spree.base_class
3
3
  include Spree::VatPriceCalculation
4
- if defined?(Spree::Webhooks::HasWebhooks)
5
- include Spree::Webhooks::HasWebhooks
6
- end
4
+
5
+ publishes_lifecycle_events
7
6
 
8
7
  acts_as_paranoid
9
8
 
10
9
  MAXIMUM_AMOUNT = BigDecimal('99_999_999.99')
11
10
 
12
11
  belongs_to :variant, -> { with_deleted }, class_name: 'Spree::Variant', inverse_of: :prices, touch: true
12
+ belongs_to :price_list, class_name: 'Spree::PriceList', optional: true
13
13
 
14
14
  before_validation :ensure_currency
15
15
  before_save :remove_compare_at_amount_if_equals_amount
@@ -20,11 +20,11 @@ module Spree
20
20
  less_than_or_equal_to: MAXIMUM_AMOUNT
21
21
  }, if: -> { Spree::Config.allow_empty_price_amount }
22
22
 
23
- # new behavior
23
+ # new behavior - prices on a price_list can have nil amounts (placeholder prices)
24
24
  validates :amount, allow_nil: false, numericality: {
25
25
  greater_than_or_equal_to: 0,
26
26
  less_than_or_equal_to: MAXIMUM_AMOUNT
27
- }, unless: -> { Spree::Config.allow_empty_price_amount }
27
+ }, unless: -> { Spree::Config.allow_empty_price_amount || price_list_id.present? }
28
28
 
29
29
  validates :compare_at_amount, allow_nil: true, numericality: {
30
30
  greater_than_or_equal_to: 0,
@@ -36,6 +36,8 @@ module Spree
36
36
  scope :with_currency, ->(currency) { where(currency: currency) }
37
37
  scope :non_zero, -> { where.not(amount: [nil, 0]) }
38
38
  scope :discounted, -> { where('compare_at_amount > amount') }
39
+ scope :base_prices, -> { where(price_list_id: nil) }
40
+ scope :for_price_list, ->(price_list) { where(price_list_id: price_list) }
39
41
  scope :for_products, ->(products, currency = nil) do
40
42
  currency ||= Spree::Store.default.default_currency
41
43
 
@@ -59,7 +61,7 @@ module Spree
59
61
  end
60
62
 
61
63
  def amount=(amount)
62
- self[:amount] = Spree::LocalizedNumber.parse(amount)
64
+ self[:amount] = amount.blank? ? nil : Spree::LocalizedNumber.parse(amount)
63
65
  end
64
66
 
65
67
  def compare_at_money