solidus_core 2.11.10 → 3.3.1

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 (252) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +6 -2
  3. data/app/assets/javascripts/spree.js.erb +0 -51
  4. data/app/controllers/spree/base_controller.rb +1 -1
  5. data/app/helpers/spree/base_helper.rb +1 -1
  6. data/app/helpers/spree/products_helper.rb +2 -2
  7. data/app/helpers/spree/store_helper.rb +0 -11
  8. data/app/mailers/spree/carton_mailer.rb +1 -5
  9. data/app/models/concerns/spree/active_storage_adapter/attachment.rb +30 -11
  10. data/app/models/concerns/spree/active_storage_adapter.rb +1 -1
  11. data/app/models/concerns/spree/adjustment_source.rb +0 -15
  12. data/app/models/concerns/spree/calculated_adjustments.rb +0 -18
  13. data/app/models/concerns/spree/default_price.rb +39 -10
  14. data/app/models/concerns/spree/ransackable_attributes.rb +24 -4
  15. data/app/models/concerns/spree/soft_deletable.rb +2 -4
  16. data/app/models/concerns/spree/user_address_book.rb +10 -37
  17. data/app/models/concerns/spree/user_methods.rb +38 -13
  18. data/app/models/spree/ability.rb +0 -37
  19. data/app/models/spree/address/name.rb +2 -20
  20. data/app/models/spree/address.rb +8 -186
  21. data/app/models/spree/adjustment.rb +7 -33
  22. data/app/models/spree/base.rb +0 -53
  23. data/app/models/spree/calculator/flat_fee.rb +21 -0
  24. data/app/models/spree/calculator/flexi_rate.rb +0 -5
  25. data/app/models/spree/calculator.rb +0 -7
  26. data/app/models/spree/carton.rb +1 -1
  27. data/app/models/spree/country.rb +2 -7
  28. data/app/models/spree/credit_card.rb +1 -28
  29. data/app/models/spree/customer_return.rb +5 -7
  30. data/app/models/spree/image/active_storage_attachment.rb +2 -7
  31. data/app/models/spree/image/paperclip_attachment.rb +2 -2
  32. data/app/models/spree/image.rb +0 -7
  33. data/app/models/spree/inventory_unit.rb +0 -21
  34. data/app/models/spree/line_item.rb +6 -49
  35. data/app/models/spree/log_entry.rb +74 -1
  36. data/app/models/spree/option_type.rb +1 -1
  37. data/app/models/spree/option_value.rb +10 -1
  38. data/app/models/spree/order/number_generator.rb +7 -1
  39. data/app/models/spree/order.rb +82 -170
  40. data/app/models/spree/order_cancellations.rb +4 -24
  41. data/app/models/spree/order_contents.rb +2 -1
  42. data/app/models/spree/order_inventory.rb +1 -1
  43. data/app/models/spree/order_merger.rb +2 -2
  44. data/app/models/spree/order_promotion.rb +1 -1
  45. data/app/models/spree/order_shipping.rb +6 -9
  46. data/app/models/spree/order_taxation.rb +6 -4
  47. data/app/models/spree/order_updater.rb +17 -16
  48. data/app/models/spree/payment/cancellation.rb +1 -1
  49. data/app/models/spree/payment/processing.rb +58 -55
  50. data/app/models/spree/payment.rb +0 -3
  51. data/app/models/spree/payment_create.rb +1 -13
  52. data/app/models/spree/payment_method/bogus_credit_card.rb +6 -7
  53. data/app/models/spree/payment_method/credit_card.rb +1 -3
  54. data/app/models/spree/payment_method/simple_bogus_credit_card.rb +8 -0
  55. data/app/models/spree/payment_method.rb +26 -110
  56. data/app/models/spree/price.rb +3 -3
  57. data/app/models/spree/product/scopes.rb +24 -33
  58. data/app/models/spree/product.rb +15 -42
  59. data/app/models/spree/product_property.rb +1 -1
  60. data/app/models/spree/promotion/actions/create_adjustment.rb +4 -3
  61. data/app/models/spree/promotion/actions/create_item_adjustments.rb +5 -9
  62. data/app/models/spree/promotion/actions/create_quantity_adjustments.rb +0 -3
  63. data/app/models/spree/promotion/actions/free_shipping.rb +1 -0
  64. data/app/models/spree/promotion/order_adjustments_recalculator.rb +92 -0
  65. data/app/models/spree/promotion/rules/item_total.rb +50 -6
  66. data/app/models/spree/promotion/rules/product.rb +20 -8
  67. data/app/models/spree/promotion/rules/store.rb +4 -0
  68. data/app/models/spree/promotion/rules/taxon.rb +6 -15
  69. data/app/models/spree/promotion/rules/user.rb +4 -0
  70. data/app/models/spree/promotion.rb +39 -32
  71. data/app/models/spree/promotion_action.rb +6 -9
  72. data/app/models/spree/promotion_code/batch_builder.rb +0 -14
  73. data/app/models/spree/promotion_code.rb +11 -7
  74. data/app/models/spree/promotion_handler/cart.rb +26 -6
  75. data/app/models/spree/promotion_rule.rb +5 -0
  76. data/app/models/spree/property.rb +1 -1
  77. data/app/models/spree/refund.rb +8 -52
  78. data/app/models/spree/reimbursement.rb +5 -43
  79. data/app/models/spree/reimbursement_performer.rb +2 -8
  80. data/app/models/spree/reimbursement_type/credit.rb +1 -4
  81. data/app/models/spree/reimbursement_type/reimbursement_helpers.rb +1 -2
  82. data/app/models/spree/reimbursement_type/store_credit.rb +1 -4
  83. data/app/models/spree/return_authorization.rb +2 -5
  84. data/app/models/spree/return_item.rb +4 -24
  85. data/app/models/spree/shipment.rb +3 -56
  86. data/app/models/spree/shipping_method.rb +0 -25
  87. data/app/models/spree/shipping_rate.rb +0 -2
  88. data/app/models/spree/shipping_rate_tax.rb +1 -1
  89. data/app/models/spree/state.rb +1 -5
  90. data/app/models/spree/stock/allocator/on_hand_first.rb +2 -2
  91. data/app/models/spree/stock/availability.rb +11 -3
  92. data/app/models/spree/stock/quantifier.rb +12 -8
  93. data/app/models/spree/stock/simple_coordinator.rb +8 -26
  94. data/app/models/spree/stock/splitter/base.rb +2 -7
  95. data/app/models/spree/stock_item.rb +2 -8
  96. data/app/models/spree/stock_location.rb +2 -2
  97. data/app/models/spree/stock_movement.rb +2 -2
  98. data/app/models/spree/store.rb +0 -12
  99. data/app/models/spree/store_credit.rb +14 -1
  100. data/app/models/spree/store_credit_category.rb +0 -32
  101. data/app/models/spree/store_credit_prioritizer.rb +17 -0
  102. data/app/models/spree/tax/item_tax.rb +3 -2
  103. data/app/models/spree/tax/order_tax.rb +3 -1
  104. data/app/models/spree/tax/tax_helpers.rb +2 -2
  105. data/app/models/spree/tax/tax_location.rb +4 -7
  106. data/app/models/spree/tax_calculator/default.rb +31 -0
  107. data/app/models/spree/tax_calculator/shipping_rate.rb +2 -13
  108. data/app/models/spree/tax_rate.rb +9 -27
  109. data/app/models/spree/taxon/active_storage_attachment.rb +2 -7
  110. data/app/models/spree/taxon/paperclip_attachment.rb +3 -8
  111. data/app/models/spree/taxon.rb +1 -12
  112. data/app/models/spree/taxonomy.rb +1 -1
  113. data/app/models/spree/user_address.rb +0 -5
  114. data/app/models/spree/user_last_url_storer/rules/authentication_rule.rb +1 -1
  115. data/app/models/spree/variant/price_selector.rb +34 -4
  116. data/app/models/spree/variant.rb +52 -66
  117. data/app/models/spree/zone.rb +1 -1
  118. data/app/subscribers/spree/mailer_subscriber.rb +4 -0
  119. data/app/subscribers/spree/order_mailer_subscriber.rb +35 -0
  120. data/config/i18n-tasks.yml +134 -0
  121. data/config/locales/en.yml +406 -263
  122. data/db/migrate/20180416083007_add_apply_to_all_to_variant_property_rule.rb +1 -1
  123. data/db/migrate/20201127212108_add_type_before_removal_to_spree_payment_methods.rb +7 -0
  124. data/db/migrate/20210312061050_change_column_null_on_prices.rb +7 -0
  125. data/db/migrate/20220317165036_set_promotions_with_any_policy_to_all_if_possible.rb +20 -0
  126. data/db/migrate/20220805202442_add_level_to_spree_tax_rates.rb +5 -0
  127. data/db/migrate/20221123152807_add_shipping_category_to_spree_variants.rb +5 -0
  128. data/db/seeds.rb +4 -1
  129. data/lib/generators/solidus/install/app_templates/authentication/custom.rb +21 -0
  130. data/lib/generators/solidus/install/app_templates/authentication/devise.rb +16 -0
  131. data/lib/generators/solidus/install/app_templates/authentication/existing.rb +10 -0
  132. data/lib/generators/solidus/install/app_templates/authentication/none.rb +1 -0
  133. data/lib/generators/solidus/install/app_templates/frontend/break_down_solidus_gem.rb +54 -0
  134. data/lib/generators/solidus/install/app_templates/frontend/classic.rb +16 -0
  135. data/lib/generators/solidus/install/app_templates/frontend/none.rb +2 -0
  136. data/lib/generators/solidus/install/app_templates/frontend/starter.rb +3 -0
  137. data/lib/generators/solidus/install/app_templates/payment_method/bolt.rb +13 -0
  138. data/lib/generators/solidus/install/app_templates/payment_method/none.rb +1 -0
  139. data/lib/generators/solidus/install/app_templates/payment_method/paypal.rb +10 -0
  140. data/lib/generators/solidus/install/install_generator.rb +247 -149
  141. data/lib/generators/solidus/install/templates/config/initializers/spree.rb.tt +15 -60
  142. data/lib/generators/solidus/install/templates/vendor/assets/javascripts/spree/backend/all.js +2 -2
  143. data/lib/generators/solidus/update/templates/config/initializers/new_solidus_defaults.rb.tt +30 -0
  144. data/lib/generators/solidus/update/update_generator.rb +112 -0
  145. data/lib/generators/spree/custom_user/custom_user_generator.rb +6 -4
  146. data/lib/generators/spree/custom_user/templates/authentication_helpers.rb.tt +2 -6
  147. data/lib/generators/spree/custom_user/templates/migration.rb.tt +7 -3
  148. data/lib/generators/spree/dummy/dummy_generator.rb +12 -9
  149. data/lib/generators/spree/dummy/templates/rails/application.rb.tt +0 -1
  150. data/lib/generators/spree/dummy/templates/rails/database.yml +81 -39
  151. data/lib/generators/spree/dummy/templates/rails/storage.yml +3 -0
  152. data/lib/generators/spree/dummy/templates/rails/test.rb +2 -0
  153. data/lib/spree/app_configuration.rb +134 -64
  154. data/lib/spree/bus.rb +20 -0
  155. data/lib/spree/core/class_constantizer.rb +2 -0
  156. data/lib/spree/core/controller_helpers/auth.rb +10 -15
  157. data/lib/spree/core/controller_helpers/current_host.rb +5 -3
  158. data/lib/spree/core/controller_helpers/order.rb +12 -32
  159. data/lib/spree/core/controller_helpers/payment_parameters.rb +0 -54
  160. data/lib/spree/core/controller_helpers/pricing.rb +0 -8
  161. data/lib/spree/core/controller_helpers/search.rb +1 -1
  162. data/lib/spree/core/controller_helpers/strong_parameters.rb +0 -4
  163. data/lib/spree/core/engine.rb +54 -50
  164. data/lib/spree/core/environment_extension.rb +0 -9
  165. data/lib/spree/core/product_filters.rb +1 -41
  166. data/lib/spree/core/role_configuration.rb +0 -14
  167. data/lib/spree/core/search/base.rb +18 -35
  168. data/lib/spree/core/state_machines/order.rb +2 -2
  169. data/lib/spree/core/state_machines.rb +2 -11
  170. data/lib/spree/core/stock_configuration.rb +18 -0
  171. data/lib/spree/core/validators/email.rb +5 -3
  172. data/lib/spree/core/version.rb +5 -1
  173. data/lib/spree/core/versioned_value.rb +75 -0
  174. data/lib/spree/core.rb +40 -11
  175. data/lib/spree/deprecation.rb +1 -1
  176. data/lib/spree/event/configuration.rb +0 -5
  177. data/lib/spree/event/subscriber.rb +0 -18
  178. data/lib/spree/event/subscriber_registry.rb +7 -7
  179. data/lib/spree/event.rb +1 -32
  180. data/lib/spree/i18n.rb +0 -22
  181. data/lib/spree/migrations.rb +13 -11
  182. data/lib/spree/money.rb +3 -18
  183. data/lib/spree/permission_sets/default_customer.rb +8 -1
  184. data/lib/spree/permitted_attributes.rb +17 -59
  185. data/lib/spree/preferences/configuration.rb +84 -0
  186. data/lib/spree/preferences/preferable.rb +13 -0
  187. data/lib/spree/preferences/preferable_class_methods.rb +37 -4
  188. data/lib/spree/preferences/preference_differentiator.rb +29 -0
  189. data/lib/spree/preferences/static_model_preferences.rb +25 -10
  190. data/lib/spree/rails_compatibility.rb +106 -0
  191. data/lib/spree/testing_support/blacklist_urls.rb +1 -1
  192. data/lib/spree/testing_support/bus_helpers.rb +101 -0
  193. data/lib/spree/testing_support/capybara_ext.rb +0 -30
  194. data/lib/spree/testing_support/common_rake.rb +71 -23
  195. data/lib/spree/testing_support/controller_requests.rb +0 -82
  196. data/lib/spree/testing_support/dummy_app/assets/javascripts/spree/backend/all.js +1 -1
  197. data/lib/spree/testing_support/dummy_app/assets/javascripts/spree/frontend/all.js +1 -1
  198. data/lib/spree/testing_support/dummy_app/database.yml +42 -22
  199. data/lib/spree/testing_support/dummy_app/migrations.rb +0 -3
  200. data/lib/spree/testing_support/dummy_app.rb +47 -34
  201. data/lib/spree/testing_support/factories/address_factory.rb +9 -6
  202. data/lib/spree/testing_support/factories/calculator_factory.rb +3 -0
  203. data/lib/spree/testing_support/factories/country_factory.rb +1 -2
  204. data/lib/spree/testing_support/factories/inventory_unit_factory.rb +1 -1
  205. data/lib/spree/testing_support/factories/order_factory.rb +8 -5
  206. data/lib/spree/testing_support/factories/product_factory.rb +4 -1
  207. data/lib/spree/testing_support/factories/promotion_factory.rb +28 -14
  208. data/lib/spree/testing_support/factories/refund_factory.rb +0 -1
  209. data/lib/spree/testing_support/factories/state_factory.rb +8 -2
  210. data/lib/spree/testing_support/factories/store_credit_factory.rb +4 -4
  211. data/lib/spree/testing_support/factories/user_factory.rb +6 -0
  212. data/lib/spree/testing_support/factory_bot.rb +2 -2
  213. data/lib/spree/testing_support/order_walkthrough.rb +6 -8
  214. data/lib/spree/testing_support/preferences.rb +0 -25
  215. data/lib/spree/testing_support/silence_deprecations.rb +9 -0
  216. data/lib/tasks/colorado_delivery_fee.rake +28 -0
  217. data/lib/tasks/payment_method.rake +29 -0
  218. data/lib/tasks/solidus/check_orders_with_invalid_email.rake +18 -0
  219. data/lib/tasks/solidus/delete_prices_with_nil_amount.rake +8 -0
  220. data/lib/tasks/solidus/split_promotions_with_any_match_policy.rake +33 -0
  221. data/solidus_core.gemspec +14 -7
  222. metadata +127 -78
  223. data/app/mailers/spree/test_mailer.rb +0 -13
  224. data/app/models/concerns/spree/user_payment_source.rb +0 -26
  225. data/app/models/spree/calculator/free_shipping.rb +0 -22
  226. data/app/models/spree/calculator/percent_per_item.rb +0 -51
  227. data/app/models/spree/calculator/price_sack.rb +0 -28
  228. data/app/models/spree/gateway/bogus.rb +0 -13
  229. data/app/models/spree/gateway/bogus_simple.rb +0 -13
  230. data/app/models/spree/gateway.rb +0 -14
  231. data/app/models/spree/order/checkout.rb +0 -244
  232. data/app/models/spree/order_capturing.rb +0 -50
  233. data/app/models/spree/promotion_handler/free_shipping.rb +0 -9
  234. data/app/models/spree/tax/shipping_rate_taxer.rb +0 -24
  235. data/lib/generators/solidus/install/templates/vendor/assets/javascripts/spree/frontend/all.js +0 -10
  236. data/lib/generators/solidus/install/templates/vendor/assets/stylesheets/spree/frontend/all.css +0 -9
  237. data/lib/generators/spree/install/install_generator.rb +0 -15
  238. data/lib/solidus/migrations/rename_gateways.rb +0 -41
  239. data/lib/spree/core/current_store.rb +0 -24
  240. data/lib/spree/paranoia_deprecations.rb +0 -41
  241. data/lib/spree/promo/environment.rb +0 -12
  242. data/lib/spree/testing_support/bar_ability.rb +0 -19
  243. data/lib/tasks/core.rake +0 -104
  244. data/lib/tasks/email.rake +0 -12
  245. data/lib/tasks/migrations/copy_order_bill_address_to_credit_card.rake +0 -119
  246. data/lib/tasks/migrations/migrate_address_names.rake +0 -158
  247. data/lib/tasks/migrations/migrate_default_billing_addresses_to_address_book.rake +0 -26
  248. data/lib/tasks/migrations/migrate_shipping_rate_taxes.rake +0 -22
  249. data/lib/tasks/migrations/migrate_user_addresses.rake +0 -34
  250. data/lib/tasks/migrations/rename_gateways.rake +0 -23
  251. data/lib/tasks/order_capturing.rake +0 -27
  252. data/lib/tasks/upgrade.rake +0 -13
@@ -5,10 +5,6 @@ module Spree
5
5
  # variations, called variants. Product properties include description,
6
6
  # permalink, availability, shipping category, etc. that do not change by
7
7
  # variant.
8
- #
9
- # @note this model uses {https://github.com/radar/paranoia paranoia}.
10
- # +#destroy+ will only soft-destroy records and the default scope hides
11
- # soft-destroyed records using +WHERE deleted_at IS NULL+.
12
8
  class Product < Spree::Base
13
9
  extend FriendlyId
14
10
  friendly_id :slug_candidates, use: :history
@@ -65,6 +61,17 @@ module Spree
65
61
  has_many :line_items, through: :variants_including_master
66
62
  has_many :orders, through: :line_items
67
63
 
64
+ scope :sort_by_master_default_price_amount_asc, -> {
65
+ with_default_price.order('spree_prices.amount ASC')
66
+ }
67
+ scope :sort_by_master_default_price_amount_desc, -> {
68
+ with_default_price.order('spree_prices.amount DESC')
69
+ }
70
+ scope :with_default_price, -> {
71
+ left_joins(master: :prices)
72
+ .where(master: { spree_prices: Spree::Config.default_pricing_options.desired_attributes })
73
+ }
74
+
68
75
  def find_or_build_master
69
76
  master || build_master
70
77
  end
@@ -89,7 +96,7 @@ module Spree
89
96
  :has_default_price?,
90
97
  :images,
91
98
  :price_for,
92
- :price_in,
99
+ :price_for_options,
93
100
  :rebuild_vat_prices=,
94
101
  to: :find_or_build_master
95
102
 
@@ -124,11 +131,11 @@ module Spree
124
131
 
125
132
  alias :options :product_option_types
126
133
 
127
- self.whitelisted_ransackable_associations = %w[stores variants_including_master master variants]
128
- self.whitelisted_ransackable_attributes = %w[name slug]
134
+ self.allowed_ransackable_associations = %w[stores variants_including_master master variants]
135
+ self.allowed_ransackable_attributes = %w[name slug]
129
136
 
130
137
  def self.ransackable_scopes(_auth_object = nil)
131
- %i(with_discarded with_variant_sku_cont)
138
+ %i(available with_discarded with_variant_sku_cont with_all_variant_sku_cont with_kept_variant_sku_cont)
132
139
  end
133
140
 
134
141
  # @return [Boolean] true if there are any variants
@@ -186,19 +193,6 @@ module Spree
186
193
  !!discontinue_on&.past?
187
194
  end
188
195
 
189
- # Groups variants by the specified option type.
190
- #
191
- # @deprecated This method is not called in the Solidus codebase
192
- # @param opt_type [String] the name of the option type to group by
193
- # @param pricing_options [Spree::Config.pricing_options_class] the pricing options to search
194
- # for, default: the default pricing options
195
- # @return [Hash] option_type as keys, array of variants as values.
196
- def categorise_variants_from_option(opt_type, pricing_options = Spree::Config.default_pricing_options)
197
- return {} unless option_types.include?(opt_type)
198
- variants.with_prices(pricing_options).group_by { |variant| variant.option_values.detect { |option| option.option_type == opt_type } }
199
- end
200
- deprecate :categorise_variants_from_option, deprecator: Spree::Deprecation
201
-
202
196
  # Poor man's full text search.
203
197
  #
204
198
  # Filters products to those which have any of the strings in +values+ in
@@ -214,17 +208,6 @@ module Spree
214
208
  where conditions.inject(:or)
215
209
  end
216
210
 
217
- # @param current_currency [String] currency to filter variants by; defaults to Spree's default
218
- # @deprecated This method can only handle prices for currencies
219
- # @return [Array<Spree::Variant>] all variants with at least one option value
220
- def variants_and_option_values(current_currency = nil)
221
- variants.includes(:option_values).active(current_currency).select do |variant|
222
- variant.option_values.any?
223
- end
224
- end
225
- deprecate variants_and_option_values: :variants_and_option_values_for,
226
- deprecator: Spree::Deprecation
227
-
228
211
  # @param pricing_options [Spree::Variant::PricingOptions] the pricing options to search
229
212
  # for, default: the default pricing options
230
213
  # @return [Array<Spree::Variant>] all variants with at least one option value
@@ -296,16 +279,6 @@ module Spree
296
279
  end
297
280
  end
298
281
 
299
- # Image that can be used for the product.
300
- #
301
- # Will first search for images on the product, then those belonging to the
302
- # variants. If all else fails, will return a new image object.
303
- # @return [Spree::Image] the image to display
304
- def display_image
305
- Spree::Deprecation.warn('Spree::Product#display_image is DEPRECATED. Choose an image from Spree::Product#gallery instead.')
306
- images.first || variant_images.first || Spree::Image.new
307
- end
308
-
309
282
  # Finds the variant property rule that matches the provided option value ids.
310
283
  #
311
284
  # @param option_value_ids [Array<Integer>] list of option value ids
@@ -9,6 +9,6 @@ module Spree
9
9
  belongs_to :product, touch: true, class_name: 'Spree::Product', inverse_of: :product_properties, optional: true
10
10
  belongs_to :property, class_name: 'Spree::Property', inverse_of: :product_properties, optional: true
11
11
 
12
- self.whitelisted_ransackable_attributes = ['value']
12
+ self.allowed_ransackable_attributes = ['value']
13
13
  end
14
14
  end
@@ -15,6 +15,10 @@ module Spree
15
15
  before_destroy :remove_adjustments_from_incomplete_orders
16
16
  before_discard :remove_adjustments_from_incomplete_orders
17
17
 
18
+ def preload_relations
19
+ [:calculator]
20
+ end
21
+
18
22
  # Creates the adjustment related to a promotion for the order passed
19
23
  # through options hash
20
24
  #
@@ -39,9 +43,6 @@ module Spree
39
43
  # item_total and ship_total
40
44
  def compute_amount(calculable)
41
45
  amount = calculator.compute(calculable)
42
- if !amount.is_a?(BigDecimal)
43
- Spree::Deprecation.warn "#{calculator.class.name}#compute returned #{amount.inspect}, it should return a BigDecimal"
44
- end
45
46
  amount ||= BigDecimal(0)
46
47
  amount = amount.abs
47
48
  [(calculable.item_total + calculable.ship_total), amount].min * -1
@@ -15,6 +15,10 @@ module Spree
15
15
  before_destroy :remove_adjustments_from_incomplete_orders
16
16
  before_discard :remove_adjustments_from_incomplete_orders
17
17
 
18
+ def preload_relations
19
+ [:calculator]
20
+ end
21
+
18
22
  def perform(payload = {})
19
23
  order = payload[:order]
20
24
  promotion = payload[:promotion]
@@ -33,9 +37,6 @@ module Spree
33
37
  order = adjustable.is_a?(Order) ? adjustable : adjustable.order
34
38
  return 0 unless promotion.line_item_actionable?(order, adjustable)
35
39
  promotion_amount = calculator.compute(adjustable)
36
- if !promotion_amount.is_a?(BigDecimal)
37
- Spree::Deprecation.warn "#{calculator.class.name}#compute returned #{promotion_amount.inspect}, it should return a BigDecimal"
38
- end
39
40
  promotion_amount ||= BigDecimal(0)
40
41
  promotion_amount = promotion_amount.abs
41
42
  [adjustable.amount, promotion_amount].min * -1
@@ -86,13 +87,8 @@ module Spree
86
87
  end
87
88
 
88
89
  def line_items_to_adjust(promotion, order)
89
- excluded_ids = adjustments.
90
- where(adjustable_id: order.line_items.pluck(:id), adjustable_type: 'Spree::LineItem').
91
- pluck(:adjustable_id).
92
- to_set
93
-
94
90
  order.line_items.select do |line_item|
95
- !excluded_ids.include?(line_item.id) &&
91
+ line_item.adjustments.none? { |adjustment| adjustment.source == self } &&
96
92
  promotion.line_item_actionable?(order, line_item)
97
93
  end
98
94
  end
@@ -57,9 +57,6 @@ module Spree
57
57
  #
58
58
  def compute_amount(line_item)
59
59
  adjustment_amount = calculator.compute(PartialLineItem.new(line_item))
60
- if !adjustment_amount.is_a?(BigDecimal)
61
- Spree::Deprecation.warn "#{calculator.class.name}#compute returned #{adjustment_amount.inspect}, it should return a BigDecimal"
62
- end
63
60
  adjustment_amount ||= BigDecimal(0)
64
61
  adjustment_amount = adjustment_amount.abs
65
62
 
@@ -7,6 +7,7 @@ module Spree
7
7
  def perform(payload = {})
8
8
  order = payload[:order]
9
9
  promotion_code = payload[:promotion_code]
10
+ return false unless promotion.eligible? order
10
11
 
11
12
  created_adjustments = order.shipments.map do |shipment|
12
13
  next if promotion_credit_exists?(shipment)
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ class Promotion < Spree::Base
5
+
6
+ # This class encapsulates all the things the promotion system does to
7
+ # an order. It is called from the `Spree::OrderUpdater` before taxes are
8
+ # calculated, such that taxes always respect promotions.
9
+
10
+ # This class iterates over all existing promotion adjustments and recalculates
11
+ # their amount and eligibility using their adjustment source.
12
+ class OrderAdjustmentsRecalculator
13
+ def initialize(order)
14
+ @order = order
15
+ end
16
+
17
+ def call
18
+ all_items = line_items + shipments
19
+ all_items.each do |item|
20
+ promotion_adjustments = item.adjustments.select(&:promotion?)
21
+
22
+ promotion_adjustments.each { |adjustment| recalculate(adjustment) }
23
+ Spree::Config.promotion_chooser_class.new(promotion_adjustments).update
24
+
25
+ item.promo_total = promotion_adjustments.select(&:eligible?).sum(&:amount)
26
+ end
27
+ # Update and select the best promotion adjustment for the order.
28
+ # We don't update the order.promo_total yet. Order totals are updated later
29
+ # in #update_adjustment_total since they include the totals from the order's
30
+ # line items and/or shipments.
31
+ order_promotion_adjustments = order.adjustments.select(&:promotion?)
32
+ order_promotion_adjustments.each { |adjustment| recalculate(adjustment) }
33
+ Spree::Config.promotion_chooser_class.new(order_promotion_adjustments).update
34
+
35
+ order.promo_total = all_items.sum(&:promo_total) +
36
+ order_promotion_adjustments.
37
+ select(&:eligible?).
38
+ select(&:promotion?).
39
+ sum(&:amount)
40
+ order
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader :order
46
+ delegate :line_items, :shipments, to: :order
47
+
48
+ # Recalculate and persist the amount from this adjustment's source based on
49
+ # the adjustable ({Order}, {Shipment}, or {LineItem})
50
+ #
51
+ # If the adjustment has no source (such as when created manually from the
52
+ # admin) or is closed, this is a noop.
53
+ #
54
+ # @return [BigDecimal] New amount of this adjustment
55
+ def recalculate(adjustment)
56
+ if adjustment.finalized?
57
+ return adjustment.amount
58
+ end
59
+
60
+ # If the adjustment has no source, do not attempt to re-calculate the
61
+ # amount.
62
+ # Some scenarios where this happens:
63
+ # - Adjustments that are manually created via the admin backend
64
+ # - PromotionAction adjustments where the PromotionAction was deleted
65
+ # after the order was completed.
66
+ if adjustment.source.present?
67
+ adjustment.amount = adjustment.source.compute_amount(adjustment.adjustable)
68
+
69
+ adjustment.eligible = calculate_eligibility(adjustment)
70
+
71
+ # Persist only if changed
72
+ # This is only not a save! to avoid the extra queries to load the order
73
+ # (for validations) and to touch the adjustment.
74
+ adjustment.update_columns(eligible: adjustment.eligible, amount: adjustment.amount, updated_at: Time.current) if adjustment.changed?
75
+ end
76
+ adjustment.amount
77
+ end
78
+
79
+ # Calculates based on attached promotion (if this is a promotion
80
+ # adjustment) whether this promotion is still eligible.
81
+ # @api private
82
+ # @return [true,false] Whether this adjustment is eligible
83
+ def calculate_eligibility(adjustment)
84
+ if !adjustment.finalized? && adjustment.source
85
+ adjustment.source.promotion.eligible?(adjustment.adjustable, promotion_code: adjustment.promotion_code)
86
+ else
87
+ adjustment.eligible?
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -5,12 +5,38 @@ module Spree
5
5
  module Rules
6
6
  # A rule to apply to an order greater than (or greater than or equal to)
7
7
  # a specific amount
8
+ #
9
+ # To add extra operators please override `self.operators_map` or any other helper method.
10
+ # To customize the error message you can also override `ineligible_message`.
8
11
  class ItemTotal < PromotionRule
12
+ include ActiveSupport::Deprecation::DeprecatedConstantAccessor
13
+
9
14
  preference :amount, :decimal, default: 100.00
10
15
  preference :currency, :string, default: ->{ Spree::Config[:currency] }
11
- preference :operator, :string, default: '>'
16
+ preference :operator, :string, default: 'gt'
17
+
18
+ # The list of allowed operators names mapped to their symbols.
19
+ def self.operators_map
20
+ {
21
+ gte: :>=,
22
+ gt: :>,
23
+ }
24
+ end
25
+
26
+ def self.operator_options
27
+ operators_map.map do |name, _method|
28
+ [I18n.t(name, scope: 'spree.item_total_rule.operators'), name]
29
+ end
30
+ end
12
31
 
13
- OPERATORS = ['gt', 'gte']
32
+ # @deprecated
33
+ OPERATORS = operators_map.keys.map(&:to_s)
34
+ deprecate_constant(
35
+ :OPERATORS,
36
+ :operators_map,
37
+ message: "OPERATORS is deprecated! Use `operators_map.keys.map(&:to_s)` instead.",
38
+ deprecator: Spree::Deprecation,
39
+ )
14
40
 
15
41
  def applicable?(promotable)
16
42
  promotable.is_a?(Spree::Order)
@@ -18,8 +44,8 @@ module Spree
18
44
 
19
45
  def eligible?(order, _options = {})
20
46
  return false unless order.currency == preferred_currency
21
- item_total = order.item_total
22
- unless item_total.send(preferred_operator == 'gte' ? :>= : :>, BigDecimal(preferred_amount.to_s))
47
+
48
+ unless total_for_order(order).send(operator, threshold)
23
49
  eligibility_errors.add(:base, ineligible_message, error_code: ineligible_error_code)
24
50
  end
25
51
 
@@ -28,15 +54,33 @@ module Spree
28
54
 
29
55
  private
30
56
 
57
+ def operator
58
+ self.class.operators_map.fetch(
59
+ preferred_operator.to_sym,
60
+ preferred_operator_default,
61
+ )
62
+ end
63
+
64
+ def total_for_order(order)
65
+ order.item_total
66
+ end
67
+
68
+ def threshold
69
+ BigDecimal(preferred_amount.to_s)
70
+ end
71
+
31
72
  def formatted_amount
32
73
  Spree::Money.new(preferred_amount, currency: preferred_currency).to_s
33
74
  end
34
75
 
35
76
  def ineligible_message
36
- if preferred_operator == 'gte'
77
+ case preferred_operator.to_s
78
+ when 'gte'
37
79
  eligibility_error_message(:item_total_less_than, amount: formatted_amount)
38
- else
80
+ when 'gt'
39
81
  eligibility_error_message(:item_total_less_than_or_equal, amount: formatted_amount)
82
+ else
83
+ eligibility_error_message(:item_total_doesnt_match_with_operator, amount: formatted_amount, operator: preferred_operator)
40
84
  end
41
85
  end
42
86
 
@@ -12,6 +12,10 @@ module Spree
12
12
  class_name: 'Spree::ProductPromotionRule'
13
13
  has_many :products, class_name: 'Spree::Product', through: :product_promotion_rules
14
14
 
15
+ def preload_relations
16
+ [:products]
17
+ end
18
+
15
19
  MATCH_POLICIES = %w(any all none)
16
20
 
17
21
  validates_inclusion_of :preferred_match_policy, in: MATCH_POLICIES
@@ -31,17 +35,19 @@ module Spree
31
35
  return true if eligible_products.empty?
32
36
 
33
37
  case preferred_match_policy
34
- when 'all'
35
- unless eligible_products.all? { |product| order.products.include?(product) }
38
+ when "all"
39
+ unless eligible_products.all? { |product| order_products(order).include?(product) }
36
40
  eligibility_errors.add(:base, eligibility_error_message(:missing_product), error_code: :missing_product)
37
41
  end
38
- when 'any'
39
- unless order.products.any? { |product| eligible_products.include?(product) }
40
- eligibility_errors.add(:base, eligibility_error_message(:no_applicable_products), error_code: :no_applicable_products)
42
+ when "any"
43
+ unless order_products(order).any? { |product| eligible_products.include?(product) }
44
+ eligibility_errors.add(:base, eligibility_error_message(:no_applicable_products),
45
+ error_code: :no_applicable_products)
41
46
  end
42
- when 'none'
43
- unless order.products.none? { |product| eligible_products.include?(product) }
44
- eligibility_errors.add(:base, eligibility_error_message(:has_excluded_product), error_code: :has_excluded_product)
47
+ when "none"
48
+ unless order_products(order).none? { |product| eligible_products.include?(product) }
49
+ eligibility_errors.add(:base, eligibility_error_message(:has_excluded_product),
50
+ error_code: :has_excluded_product)
45
51
  end
46
52
  else
47
53
  raise "unexpected match policy: #{preferred_match_policy.inspect}"
@@ -68,6 +74,12 @@ module Spree
68
74
  def product_ids_string=(product_ids)
69
75
  self.product_ids = product_ids.to_s.split(',').map(&:strip)
70
76
  end
77
+
78
+ private
79
+
80
+ def order_products(order)
81
+ order.line_items.map(&:variant).map(&:product)
82
+ end
71
83
  end
72
84
  end
73
85
  end
@@ -9,6 +9,10 @@ module Spree
9
9
  dependent: :destroy
10
10
  has_many :stores, through: :promotion_rule_stores, class_name: "Spree::Store"
11
11
 
12
+ def preload_relations
13
+ [:stores]
14
+ end
15
+
12
16
  def applicable?(promotable)
13
17
  promotable.is_a?(Spree::Order)
14
18
  end
@@ -8,6 +8,10 @@ module Spree
8
8
  dependent: :destroy
9
9
  has_many :taxons, through: :promotion_rule_taxons, class_name: 'Spree::Taxon'
10
10
 
11
+ def preload_relations
12
+ [:taxons]
13
+ end
14
+
11
15
  MATCH_POLICIES = %w(any all none)
12
16
 
13
17
  validates_inclusion_of :preferred_match_policy, in: MATCH_POLICIES
@@ -38,11 +42,7 @@ module Spree
38
42
  eligibility_errors.add(:base, eligibility_error_message(:has_excluded_taxon), error_code: :has_excluded_taxon)
39
43
  end
40
44
  else
41
- # Change this to an exception in a future version of Solidus
42
- warn_invalid_match_policy(assume: 'any')
43
- unless order_taxons.where(id: rule_taxon_ids_with_children).exists?
44
- eligibility_errors.add(:base, eligibility_error_message(:no_matching_taxons), error_code: :no_matching_taxons)
45
- end
45
+ raise "unexpected match policy: #{preferred_match_policy.inspect}"
46
46
  end
47
47
 
48
48
  eligibility_errors.empty?
@@ -60,9 +60,7 @@ module Spree
60
60
  when 'none'
61
61
  !found
62
62
  else
63
- # Change this to an exception in a future version of Solidus
64
- warn_invalid_match_policy(assume: 'any')
65
- found
63
+ raise "unexpected match policy: #{preferred_match_policy.inspect}"
66
64
  end
67
65
  end
68
66
 
@@ -77,13 +75,6 @@ module Spree
77
75
 
78
76
  private
79
77
 
80
- def warn_invalid_match_policy(assume:)
81
- Spree::Deprecation.warn(
82
- "#{self.class.name} id=#{id} has unexpected match policy #{preferred_match_policy.inspect}. "\
83
- "Interpreting it as '#{assume}'."
84
- )
85
- end
86
-
87
78
  # All taxons in an order
88
79
  def taxons_in_order(order)
89
80
  Spree::Taxon.joins(products: { variants_including_master: :line_items })
@@ -9,6 +9,10 @@ module Spree
9
9
  dependent: :destroy
10
10
  has_many :users, through: :promotion_rule_users, class_name: Spree::UserClassHandle.new
11
11
 
12
+ def preload_relations
13
+ [:users]
14
+ end
15
+
12
16
  def applicable?(promotable)
13
17
  promotable.is_a?(Spree::Order)
14
18
  end
@@ -3,6 +3,7 @@
3
3
  module Spree
4
4
  class Promotion < Spree::Base
5
5
  MATCH_POLICIES = %w(all any)
6
+
6
7
  UNACTIVATABLE_ORDER_STATES = ["complete", "awaiting_return", "returned"]
7
8
 
8
9
  attr_reader :eligibility_errors
@@ -38,11 +39,7 @@ module Spree
38
39
 
39
40
  scope :coupons, -> { joins(:codes).distinct }
40
41
  scope :advertised, -> { where(advertise: true) }
41
- scope :active, -> do
42
- return started_and_unexpired if Spree::Config.consider_actionless_promotion_active == true
43
-
44
- has_actions.started_and_unexpired
45
- end
42
+ scope :active, -> { has_actions.started_and_unexpired }
46
43
  scope :started_and_unexpired, -> do
47
44
  table = arel_table
48
45
  time = Time.current
@@ -51,12 +48,12 @@ module Spree
51
48
  where(table[:expires_at].eq(nil).or(table[:expires_at].gt(time)))
52
49
  end
53
50
  scope :has_actions, -> do
54
- joins(:promotion_actions)
51
+ joins(:promotion_actions).distinct
55
52
  end
56
53
  scope :applied, -> { joins(:order_promotions).distinct }
57
54
 
58
- self.whitelisted_ransackable_associations = ['codes']
59
- self.whitelisted_ransackable_attributes = %w[name path promotion_category_id]
55
+ self.allowed_ransackable_associations = ['codes']
56
+ self.allowed_ransackable_attributes = %w[name path promotion_category_id]
60
57
  def self.ransackable_scopes(*)
61
58
  %i(active)
62
59
  end
@@ -71,6 +68,19 @@ module Spree
71
68
  ).first
72
69
  end
73
70
 
71
+ # All orders that have been discounted using this promotion
72
+ def discounted_orders
73
+ Spree::Order.
74
+ joins(:all_adjustments).
75
+ where(
76
+ spree_adjustments: {
77
+ source_type: "Spree::PromotionAction",
78
+ source_id: actions.map(&:id),
79
+ eligible: true
80
+ }
81
+ ).distinct
82
+ end
83
+
74
84
  def as_json(options = {})
75
85
  options[:except] ||= :code
76
86
  super
@@ -93,7 +103,7 @@ module Spree
93
103
  end
94
104
 
95
105
  def active?
96
- started? && not_expired? && (Spree::Config.consider_actionless_promotion_active || actions.present?)
106
+ started? && not_expired? && actions.present?
97
107
  end
98
108
 
99
109
  def inactive?
@@ -155,7 +165,7 @@ module Spree
155
165
  return [] if rules.none?
156
166
 
157
167
  eligible = lambda { |rule| rule.eligible?(promotable, options) }
158
- specific_rules = rules.for(promotable)
168
+ specific_rules = rules.select { |rule| rule.applicable?(promotable) }
159
169
  return [] if specific_rules.none?
160
170
 
161
171
  if match_all?
@@ -167,6 +177,12 @@ module Spree
167
177
  end
168
178
  specific_rules
169
179
  else
180
+ Spree::Deprecation.warn(
181
+ <<~WARN
182
+ Your promotion "#{name}" with ID #{id} has a match_policy of 'any'.
183
+ This is deprecated, please split the promotion into separate promotions for each rule.
184
+ WARN
185
+ )
170
186
  unless specific_rules.any?(&eligible)
171
187
  @eligibility_errors = specific_rules.map(&:eligibility_errors).detect(&:present?)
172
188
  return nil
@@ -194,11 +210,11 @@ module Spree
194
210
  # @param excluded_orders [Array<Spree::Order>] Orders to exclude from usage count
195
211
  # @return [Integer] usage count
196
212
  def usage_count(excluded_orders: [])
197
- Spree::Adjustment.promotion.
198
- eligible.
199
- in_completed_orders(excluded_orders: excluded_orders).
200
- where(source_id: actions).
201
- count(:order_id)
213
+ discounted_orders.
214
+ complete.
215
+ where.not(id: [excluded_orders.map(&:id)]).
216
+ where.not(spree_orders: { state: :canceled }).
217
+ count
202
218
  end
203
219
 
204
220
  def line_item_actionable?(order, line_item, promotion_code: nil)
@@ -219,21 +235,12 @@ module Spree
219
235
  end
220
236
 
221
237
  def used_by?(user, excluded_orders = [])
222
- [
223
- :adjustments,
224
- :line_item_adjustments,
225
- :shipment_adjustments
226
- ].any? do |adjustment_type|
227
- user.orders.complete.joins(adjustment_type).where(
228
- spree_adjustments: {
229
- source_type: "Spree::PromotionAction",
230
- source_id: actions.map(&:id),
231
- eligible: true
232
- }
233
- ).where.not(
234
- id: excluded_orders.map(&:id)
235
- ).any?
236
- end
238
+ discounted_orders.
239
+ complete.
240
+ where.not(id: excluded_orders.map(&:id)).
241
+ where(user: user).
242
+ where.not(spree_orders: { state: :canceled }).
243
+ exists?
237
244
  end
238
245
 
239
246
  # Removes a promotion and any adjustments or other side effects from an
@@ -255,9 +262,9 @@ module Spree
255
262
  def blacklisted?(promotable)
256
263
  case promotable
257
264
  when Spree::LineItem
258
- !promotable.product.promotionable?
265
+ !promotable.variant.product.promotionable?
259
266
  when Spree::Order
260
- promotable.line_items.joins(:product).where(spree_products: { promotionable: false }).exists?
267
+ promotable.line_items.any? { |line_item| !line_item.variant.product.promotionable? }
261
268
  end
262
269
  end
263
270
 
@@ -16,6 +16,10 @@ module Spree
16
16
  scope :of_type, ->(type) { where(type: Array.wrap(type).map(&:to_s)) }
17
17
  scope :shipping, -> { of_type(Spree::Config.environment.promotions.shipping_actions.to_a) }
18
18
 
19
+ def preload_relations
20
+ []
21
+ end
22
+
19
23
  # Updates the state of the order or performs some other action depending on
20
24
  # the subclass options will contain the payload from the event that
21
25
  # activated the promotion. This will include the key :user which allows
@@ -32,15 +36,8 @@ module Spree
32
36
  #
33
37
  # @param order [Spree::Order] the order to remove the action from
34
38
  # @return [void]
35
- def remove_from(order)
36
- Spree::Deprecation.warn("#{self.class.name.inspect} does not define #remove_from. The default behavior may be incorrect and will be removed in a future version of Solidus.", caller)
37
- [order, *order.line_items, *order.shipments].each do |item|
38
- item.adjustments.each do |adjustment|
39
- if adjustment.source == self
40
- item.adjustments.destroy(adjustment)
41
- end
42
- end
43
- end
39
+ def remove_from(_order)
40
+ raise 'remove_from should be implemented in a sub-class of PromotionAction'
44
41
  end
45
42
 
46
43
  def to_partial_path