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,263 @@
1
+ module Spree
2
+ class PriceList < Spree.base_class
3
+ acts_as_paranoid
4
+ acts_as_list scope: :store_id
5
+
6
+ include Spree::SingleStoreResource
7
+
8
+ MATCH_POLICIES = %w[all any].freeze
9
+
10
+ belongs_to :store, class_name: 'Spree::Store'
11
+
12
+ has_many :price_rules, class_name: 'Spree::PriceRule', dependent: :destroy
13
+ has_many :prices, class_name: 'Spree::Price', dependent: :destroy_async
14
+ has_many :variants, -> { where(spree_prices: { deleted_at: nil }).distinct }, through: :prices, source: :variant
15
+ has_many :products, -> { where(spree_prices: { deleted_at: nil }).distinct }, through: :variants, source: :product
16
+ alias price_list_products products
17
+
18
+ # Override default nested attributes to use bulk_update_prices for performance
19
+ attr_reader :prices_attributes
20
+
21
+ # Sets the prices attributes for bulk update
22
+ # @param attributes [Array<Hash>] array of price attributes with :id, :amount, :compare_at_amount
23
+ # @return [Array<Hash>] array of price attributes with :id, :amount, :compare_at_amount
24
+ def prices_attributes=(attributes)
25
+ @prices_attributes = attributes.values
26
+ end
27
+
28
+ after_update :process_bulk_prices_update
29
+
30
+ # Processes the bulk prices update
31
+ # @return [void]
32
+ def process_bulk_prices_update
33
+ return if @prices_attributes.blank?
34
+
35
+ bulk_update_prices(@prices_attributes)
36
+ @prices_attributes = nil
37
+ end
38
+
39
+ validates :name, :store, presence: true
40
+ validates :match_policy, presence: true, inclusion: { in: MATCH_POLICIES }
41
+ validate :starts_at_before_ends_at
42
+
43
+ scope :by_position, -> { order(position: :asc) }
44
+ scope :for_store, ->(store) { where(store: store) }
45
+ scope :current, lambda { |timezone = nil|
46
+ timezone ||= Rails.application.config.time_zone
47
+ # Round to beginning of minute to enable Rails query caching
48
+ current_time = Time.current.in_time_zone(timezone).beginning_of_minute
49
+ where('starts_at IS NULL OR starts_at <= ?', current_time)
50
+ .where('ends_at IS NULL OR ends_at >= ?', current_time)
51
+ }
52
+
53
+ state_machine :status, initial: :draft do
54
+ event :activate do
55
+ transition to: :active
56
+ end
57
+
58
+ event :deactivate do
59
+ transition to: :inactive
60
+ end
61
+
62
+ event :schedule do
63
+ transition to: :scheduled
64
+ end
65
+ end
66
+
67
+ # Returns price lists applicable for a given pricing context
68
+ # - active: always applies (within date range)
69
+ # - scheduled: applies only within starts_at/ends_at date range
70
+ def self.for_context(context)
71
+ timezone = context.store&.preferred_timezone || 'UTC'
72
+ for_store(context.store)
73
+ .with_status(:active, :scheduled)
74
+ .current(timezone)
75
+ .by_position
76
+ end
77
+
78
+ def self.match_policies
79
+ MATCH_POLICIES.map { |key| [Spree.t(key), key] }
80
+ end
81
+
82
+ # Returns true if the price list is applicable to the context
83
+ # @param context [Spree::Pricing::Context]
84
+ # @return [Boolean]
85
+ def applicable?(context)
86
+ return false unless active_or_scheduled?
87
+ return false unless within_date_range?(context.date || Time.current)
88
+
89
+ rules_applicable?(context)
90
+ end
91
+
92
+ # Returns true if the price list rules are applicable to the context
93
+ # @param context [Spree::Pricing::Context]
94
+ # @return [Boolean]
95
+ def rules_applicable?(context)
96
+ return true if price_rules.none?
97
+
98
+ case match_policy
99
+ when 'all'
100
+ price_rules.all? { |rule| rule.applicable?(context) }
101
+ when 'any'
102
+ price_rules.any? { |rule| rule.applicable?(context) }
103
+ else
104
+ false
105
+ end
106
+ end
107
+
108
+ # Returns true if the price list is active or scheduled
109
+ # @return [Boolean]
110
+ def active_or_scheduled?
111
+ active? || scheduled?
112
+ end
113
+
114
+ # Returns true if the price list is currently in effect
115
+ # (active, or scheduled and within date range)
116
+ def currently_active?
117
+ active_or_scheduled? && within_date_range?(Time.current)
118
+ end
119
+
120
+ # Adds products to the price list
121
+ # Creates placeholder prices (with nil amount) for all variants and currencies
122
+ # @param product_ids [Array<String>] of product ids
123
+ # @return [void]
124
+ def add_products(product_ids)
125
+ return if product_ids.blank?
126
+
127
+ currencies = store.supported_currencies_list.map(&:iso_code)
128
+ variant_ids = Spree::Variant.eligible.where(product_id: product_ids).distinct.pluck(:id)
129
+ return if variant_ids.empty?
130
+
131
+ # Get existing variant_id/currency combinations to avoid duplicates
132
+ existing = prices.where(variant_id: variant_ids)
133
+ .pluck(:variant_id, :currency)
134
+ .to_set
135
+
136
+ now = Time.current
137
+
138
+ prices_to_insert = variant_ids.flat_map do |variant_id|
139
+ currencies.filter_map do |currency|
140
+ next if existing.include?([variant_id, currency])
141
+
142
+ {
143
+ variant_id: variant_id,
144
+ currency: currency,
145
+ amount: nil,
146
+ price_list_id: id,
147
+ created_at: now,
148
+ updated_at: now
149
+ }
150
+ end
151
+ end
152
+
153
+ return if prices_to_insert.empty?
154
+
155
+ # Use upsert_all with on_duplicate: :skip to handle race conditions
156
+ Spree::Price.upsert_all(prices_to_insert, on_duplicate: :skip)
157
+ touch_variants(variant_ids)
158
+ touch
159
+ end
160
+
161
+ # Removes products from the price list
162
+ # Hard deletes prices (not soft delete) to allow re-adding products later
163
+ # @param product_ids [Array<String>] of product ids
164
+ # @return [void]
165
+ def remove_products(product_ids)
166
+ return if product_ids.blank?
167
+
168
+ variant_ids = Spree::Variant.where(product_id: product_ids).distinct.pluck(:id)
169
+ return if variant_ids.empty?
170
+
171
+ # Use delete_all for hard delete - this bypasses acts_as_paranoid
172
+ # which is required for the unique index to work when re-adding products
173
+ prices.where(variant_id: variant_ids).delete_all
174
+ touch_variants(variant_ids)
175
+ touch
176
+ end
177
+
178
+ # Bulk update prices using upsert_all for performance
179
+ # @param prices_attributes [Array<Hash>] array of price attributes with :id, :amount, :compare_at_amount
180
+ # @return [Boolean] true if successful
181
+ def bulk_update_prices(prices_attributes)
182
+ return true if prices_attributes.blank?
183
+
184
+ records_to_upsert = []
185
+ variant_ids = Set.new
186
+
187
+ # Get current values for comparison
188
+ price_ids = prices_attributes.map { |a| a[:id] || a['id'] }.compact.map(&:to_i)
189
+ current_values = prices.where(id: price_ids).pluck(:id, :amount, :compare_at_amount).to_h { |id, amount, compare_at| [id, { amount: amount, compare_at_amount: compare_at }] }
190
+
191
+ prices_attributes.each do |attrs|
192
+ attrs = (attrs.respond_to?(:to_unsafe_h) ? attrs.to_unsafe_h : attrs.to_h).with_indifferent_access
193
+ next if attrs[:id].blank?
194
+
195
+ price_id = attrs[:id].to_i
196
+ current = current_values[price_id] || {}
197
+
198
+ # Parse amounts using LocalizedNumber for proper decimal handling
199
+ amount = attrs[:amount].present? ? Spree::LocalizedNumber.parse(attrs[:amount]) : nil
200
+ compare_at_amount = attrs[:compare_at_amount].present? ? Spree::LocalizedNumber.parse(attrs[:compare_at_amount]) : nil
201
+
202
+ # Clear compare_at_amount if it equals amount
203
+ compare_at_amount = nil if compare_at_amount == amount
204
+
205
+ # Skip if nothing changed
206
+ next if amount == current[:amount] && compare_at_amount == current[:compare_at_amount]
207
+
208
+ records_to_upsert << {
209
+ id: price_id,
210
+ variant_id: attrs[:variant_id].to_i,
211
+ currency: attrs[:currency],
212
+ amount: amount,
213
+ compare_at_amount: compare_at_amount,
214
+ price_list_id: id
215
+ }
216
+
217
+ variant_ids << attrs[:variant_id].to_i
218
+ end
219
+
220
+ return true if records_to_upsert.empty?
221
+
222
+ opts = { update_only: [:amount, :compare_at_amount], on_duplicate: :update }
223
+ opts[:unique_by] = :id unless ActiveRecord::Base.connection.adapter_name == 'Mysql2'
224
+
225
+ Spree::Price.upsert_all(records_to_upsert, **opts)
226
+
227
+ touch_variants(variant_ids.to_a)
228
+ true
229
+ end
230
+
231
+ private
232
+
233
+ # Touches the variants in a background job
234
+ # @param variant_ids [Array<String>] array of variant ids
235
+ # @return [void]
236
+ def touch_variants(variant_ids)
237
+ return if variant_ids.blank?
238
+
239
+ Spree::Variants::TouchJob.perform_later(variant_ids)
240
+ end
241
+
242
+ def starts_at_before_ends_at
243
+ return if starts_at.blank? || ends_at.blank?
244
+
245
+ if starts_at >= ends_at
246
+ errors.add(:ends_at, :must_be_after_starts_at)
247
+ end
248
+ end
249
+
250
+ # Returns true if the date is within the price list date range
251
+ # @param date [Time] the date to check
252
+ # @return [Boolean]
253
+ def within_date_range?(date)
254
+ timezone = store&.preferred_timezone || Rails.application.config.time_zone
255
+ date_in_tz = date.in_time_zone(timezone)
256
+
257
+ return false if starts_at.present? && date_in_tz < starts_at
258
+ return false if ends_at.present? && date_in_tz > ends_at
259
+
260
+ true
261
+ end
262
+ end
263
+ end
@@ -0,0 +1,26 @@
1
+ module Spree
2
+ class PriceRule < Spree.base_class
3
+ belongs_to :price_list, class_name: 'Spree::PriceList', touch: true
4
+
5
+ validates :type, :price_list,presence: true
6
+
7
+ # Returns true if the price rule is applicable to the context
8
+ # @param context [Spree::Pricing::Context]
9
+ # @return [Boolean]
10
+ def applicable?(context)
11
+ raise NotImplementedError, "#{self.class.name} must implement #applicable?"
12
+ end
13
+
14
+ # Returns the human name of the price rule
15
+ # @return [String]
16
+ def self.human_name
17
+ name.demodulize.titleize
18
+ end
19
+
20
+ # Returns the description of the price rule
21
+ # @return [String]
22
+ def self.description
23
+ ''
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,21 @@
1
+ module Spree
2
+ module PriceRules
3
+ class CustomerGroupRule < Spree::PriceRule
4
+ preference :customer_group_ids, :array, default: []
5
+
6
+ def applicable?(context)
7
+ return false unless context.user
8
+ return true if preferred_customer_group_ids.empty?
9
+
10
+ user_customer_group_ids = Spree::CustomerGroupUser.where(user_id: context.user.id).pluck(:customer_group_id).map(&:to_s)
11
+
12
+ # Compare as strings to support both integer and UUID primary keys
13
+ (preferred_customer_group_ids.map(&:to_s) & user_customer_group_ids).any?
14
+ end
15
+
16
+ def self.description
17
+ Spree.t('price_rules.customer_group_rule.description')
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ module Spree
2
+ module PriceRules
3
+ class UserRule < Spree::PriceRule
4
+ preference :user_ids, :array, default: []
5
+
6
+ def applicable?(context)
7
+ return false unless context.user
8
+ return true if preferred_user_ids.empty?
9
+
10
+ # Compare as strings to support both integer and UUID primary keys
11
+ preferred_user_ids.map(&:to_s).include?(context.user.id.to_s)
12
+ end
13
+
14
+ def self.description
15
+ 'Apply pricing to specific users'
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ module Spree
2
+ module PriceRules
3
+ class VolumeRule < Spree::PriceRule
4
+ preference :min_quantity, :integer, default: 1
5
+ preference :max_quantity, :integer, nullable: true
6
+
7
+ def applicable?(context)
8
+ return false unless context.quantity
9
+
10
+ return false if context.quantity < preferred_min_quantity
11
+ return false if preferred_max_quantity && context.quantity > preferred_max_quantity
12
+
13
+ true
14
+ end
15
+
16
+ def self.description
17
+ 'Apply pricing based on quantity purchased'
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ module Spree
2
+ module PriceRules
3
+ class ZoneRule < Spree::PriceRule
4
+ preference :zone_ids, :array, default: []
5
+
6
+ def applicable?(context)
7
+ return false unless context.zone
8
+ return true if preferred_zone_ids.empty?
9
+
10
+ # Compare as strings to support both integer and UUID primary keys
11
+ preferred_zone_ids.map(&:to_s).include?(context.zone.id.to_s)
12
+ end
13
+
14
+ def self.description
15
+ 'Apply pricing based on the tax/shipping zone'
16
+ end
17
+ end
18
+ end
19
+ end