solidus_core 4.3.5 → 4.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (211) hide show
  1. checksums.yaml +4 -4
  2. data/app/helpers/spree/base_helper.rb +2 -2
  3. data/app/mailers/spree/base_mailer.rb +1 -1
  4. data/app/mailers/spree/carton_mailer.rb +1 -1
  5. data/app/mailers/spree/order_mailer.rb +3 -3
  6. data/app/mailers/spree/reimbursement_mailer.rb +1 -1
  7. data/app/models/concerns/spree/active_storage_adapter/normalization.rb +1 -1
  8. data/app/models/concerns/spree/active_storage_adapter.rb +1 -1
  9. data/app/models/concerns/spree/display_money.rb +1 -1
  10. data/app/models/concerns/spree/ordered_property_value_list.rb +2 -2
  11. data/app/models/concerns/spree/user_address_book.rb +4 -4
  12. data/app/models/concerns/spree/user_methods.rb +3 -3
  13. data/app/models/spree/address.rb +6 -6
  14. data/app/models/spree/adjustment.rb +13 -66
  15. data/app/models/spree/calculator/returns/default_refund_amount.rb +1 -1
  16. data/app/models/spree/carton.rb +2 -2
  17. data/app/models/spree/credit_card.rb +6 -6
  18. data/app/models/spree/deprecated_configurable_class.rb +40 -0
  19. data/app/models/spree/fulfilment_changer.rb +4 -4
  20. data/app/models/spree/inventory_unit.rb +2 -2
  21. data/app/models/spree/line_item.rb +1 -1
  22. data/app/models/spree/null_promotion_adjuster.rb +13 -0
  23. data/app/models/spree/null_promotion_advertiser.rb +9 -0
  24. data/app/models/spree/null_promotion_finder.rb +9 -0
  25. data/app/models/spree/null_promotion_handler.rb +44 -0
  26. data/app/models/spree/order.rb +15 -22
  27. data/app/models/spree/order_cancellations.rb +8 -8
  28. data/app/models/spree/order_mutex.rb +2 -2
  29. data/app/models/spree/order_shipping.rb +9 -9
  30. data/app/models/spree/order_updater.rb +6 -10
  31. data/app/models/spree/payment/processing.rb +2 -2
  32. data/app/models/spree/payment.rb +2 -2
  33. data/app/models/spree/payment_create.rb +1 -1
  34. data/app/models/spree/payment_method/store_credit.rb +1 -1
  35. data/app/models/spree/payment_source.rb +5 -1
  36. data/app/models/spree/permission_set.rb +11 -0
  37. data/app/models/spree/product.rb +7 -28
  38. data/app/models/spree/refund.rb +1 -1
  39. data/app/models/spree/reimbursement.rb +4 -4
  40. data/app/models/spree/reimbursement_performer.rb +3 -3
  41. data/app/models/spree/reimbursement_tax_calculator.rb +2 -2
  42. data/app/models/spree/reimbursement_type/credit.rb +1 -1
  43. data/app/models/spree/reimbursement_type/reimbursement_helpers.rb +6 -6
  44. data/app/models/spree/reimbursement_type/store_credit.rb +1 -1
  45. data/app/models/spree/return_item.rb +9 -9
  46. data/app/models/spree/role.rb +3 -1
  47. data/app/models/spree/role_permission.rb +8 -0
  48. data/app/models/spree/shipment.rb +7 -7
  49. data/app/models/spree/shipping_category.rb +2 -0
  50. data/app/models/spree/shipping_rate.rb +1 -1
  51. data/app/models/spree/{order_contents.rb → simple_order_contents.rb} +4 -11
  52. data/app/models/spree/stock/estimator.rb +1 -1
  53. data/app/models/spree/stock/inventory_unit_builder.rb +1 -1
  54. data/app/models/spree/stock/package.rb +2 -2
  55. data/app/models/spree/stock/simple_coordinator.rb +1 -1
  56. data/app/models/spree/stock_location.rb +5 -5
  57. data/app/models/spree/store.rb +1 -1
  58. data/app/models/spree/store_credit.rb +15 -15
  59. data/app/models/spree/store_credit_event.rb +3 -3
  60. data/app/models/spree/tax_calculator/default.rb +2 -2
  61. data/app/models/spree/tax_calculator/shipping_rate.rb +1 -1
  62. data/app/models/spree/tax_category.rb +3 -1
  63. data/app/models/spree/tax_rate.rb +3 -3
  64. data/app/models/spree/taxonomy.rb +2 -2
  65. data/app/models/spree/unit_cancel.rb +1 -2
  66. data/app/models/spree/variant/vat_price_generator.rb +1 -1
  67. data/app/models/spree/variant.rb +7 -2
  68. data/app/models/spree/wallet.rb +2 -2
  69. data/app/views/spree/order_mailer/cancel_email.html.erb +1 -1
  70. data/app/views/spree/order_mailer/cancel_email.text.erb +1 -1
  71. data/app/views/spree/order_mailer/confirm_email.html.erb +5 -5
  72. data/app/views/spree/order_mailer/confirm_email.text.erb +5 -5
  73. data/config/locales/en.yml +4 -90
  74. data/db/default/spree/permission_sets.rb +10 -0
  75. data/db/default/spree/return_reasons.rb +3 -1
  76. data/db/default/spree/states.rb +1 -1
  77. data/db/migrate/20160101010000_solidus_one_four.rb +0 -117
  78. data/db/migrate/20240821173254_create_spree_permission_sets_in_core.rb +9 -0
  79. data/db/migrate/20240821173341_create_spree_roles_permissions_in_core.rb +9 -0
  80. data/db/migrate/20240821173641_add_description_to_spree_roles.rb +5 -0
  81. data/db/migrate/20240904152041_add_privilege_and_category_to_spree_permission_sets.rb +6 -0
  82. data/db/seeds.rb +1 -0
  83. data/lib/generators/solidus/install/app_templates/frontend/starter.rb +1 -1
  84. data/lib/generators/solidus/update/update_generator.rb +1 -1
  85. data/lib/generators/spree/dummy/templates/rails/test.rb +6 -1
  86. data/lib/spree/app_configuration.rb +53 -84
  87. data/lib/spree/core/class_constantizer.rb +2 -2
  88. data/lib/spree/core/controller_helpers/payment_parameters.rb +1 -1
  89. data/lib/spree/core/engine.rb +1 -0
  90. data/lib/spree/core/environment/calculators.rb +35 -3
  91. data/lib/spree/core/environment/promotions.rb +25 -4
  92. data/lib/spree/core/environment_extension.rb +16 -2
  93. data/lib/spree/core/importer/order.rb +5 -5
  94. data/lib/spree/core/importer/product.rb +3 -3
  95. data/lib/spree/core/nested_class_set.rb +28 -0
  96. data/lib/spree/core/null_promotion_configuration.rb +84 -0
  97. data/lib/spree/core/product_filters.rb +1 -1
  98. data/lib/spree/core/search/variant.rb +2 -2
  99. data/lib/spree/core/state_machines/order.rb +0 -1
  100. data/lib/spree/core/validators/email.rb +1 -1
  101. data/lib/spree/core/version.rb +2 -2
  102. data/lib/spree/core.rb +2 -6
  103. data/lib/spree/deprecator.rb +9 -0
  104. data/lib/spree/i18n.rb +1 -1
  105. data/lib/spree/mailer_previews/carton_preview.rb +1 -1
  106. data/lib/spree/permission_sets/base.rb +13 -1
  107. data/lib/spree/permission_sets/configuration_display.rb +10 -0
  108. data/lib/spree/permission_sets/configuration_management.rb +10 -0
  109. data/lib/spree/permission_sets/dashboard_display.rb +12 -0
  110. data/lib/spree/permission_sets/default_customer.rb +11 -1
  111. data/lib/spree/permission_sets/order_display.rb +10 -0
  112. data/lib/spree/permission_sets/order_management.rb +10 -0
  113. data/lib/spree/permission_sets/product_display.rb +10 -0
  114. data/lib/spree/permission_sets/product_management.rb +10 -0
  115. data/lib/spree/permission_sets/restricted_stock_display.rb +10 -0
  116. data/lib/spree/permission_sets/restricted_stock_management.rb +10 -0
  117. data/lib/spree/permission_sets/stock_display.rb +10 -0
  118. data/lib/spree/permission_sets/stock_management.rb +10 -0
  119. data/lib/spree/permission_sets/super_user.rb +10 -0
  120. data/lib/spree/permission_sets/user_display.rb +10 -0
  121. data/lib/spree/permission_sets/user_management.rb +10 -0
  122. data/lib/spree/permission_sets.rb +0 -2
  123. data/lib/spree/permitted_attributes.rb +2 -2
  124. data/lib/spree/preferences/store.rb +2 -2
  125. data/lib/spree/testing_support/capybara_ext.rb +1 -1
  126. data/lib/spree/testing_support/dummy_ability.rb +7 -0
  127. data/lib/spree/testing_support/dummy_app/database.yml +2 -2
  128. data/lib/spree/testing_support/dummy_app/migrations.rb +8 -15
  129. data/lib/spree/testing_support/dummy_app/rake_tasks.rb +2 -2
  130. data/lib/spree/testing_support/dummy_app.rb +17 -12
  131. data/lib/spree/testing_support/factories/adjustment_factory.rb +1 -2
  132. data/lib/spree/testing_support/factories/calculator_factory.rb +1 -5
  133. data/lib/spree/testing_support/factories/carton_factory.rb +2 -2
  134. data/lib/spree/testing_support/factories/customer_return_factory.rb +2 -2
  135. data/lib/spree/testing_support/factories/inventory_unit_factory.rb +3 -3
  136. data/lib/spree/testing_support/factories/order_factory.rb +5 -24
  137. data/lib/spree/testing_support/factories/product_factory.rb +1 -1
  138. data/lib/spree/testing_support/factories/shipment_factory.rb +1 -1
  139. data/lib/spree/testing_support/factories/stock_item_factory.rb +5 -1
  140. data/lib/spree/testing_support/factories/stock_package_factory.rb +1 -1
  141. data/lib/spree/testing_support/factories/user_factory.rb +1 -1
  142. data/lib/spree/testing_support/factories/zone_factory.rb +1 -1
  143. data/lib/spree/testing_support/order_walkthrough.rb +3 -3
  144. data/lib/spree/testing_support/shared_examples/calculator.rb +10 -0
  145. data/lib/spree/testing_support/shared_examples/order_factory.rb +141 -0
  146. data/lib/spree/testing_support/shared_examples/working_factory.rb +15 -0
  147. data/lib/tasks/colorado_delivery_fee.rake +2 -2
  148. data/solidus_core.gemspec +6 -6
  149. metadata +60 -91
  150. data/app/jobs/spree/promotion_code_batch_job.rb +0 -26
  151. data/app/mailers/spree/promotion_code_batch_mailer.rb +0 -15
  152. data/app/models/spree/calculator/distributed_amount.rb +0 -33
  153. data/app/models/spree/calculator/flat_percent_item_total.rb +0 -23
  154. data/app/models/spree/calculator/flexi_rate.rb +0 -22
  155. data/app/models/spree/calculator/percent_on_line_item.rb +0 -13
  156. data/app/models/spree/calculator/tiered_flat_rate.rb +0 -52
  157. data/app/models/spree/calculator/tiered_percent.rb +0 -62
  158. data/app/models/spree/order_promotion.rb +0 -27
  159. data/app/models/spree/promotion/actions/create_adjustment.rb +0 -81
  160. data/app/models/spree/promotion/actions/create_item_adjustments.rb +0 -98
  161. data/app/models/spree/promotion/actions/create_quantity_adjustments.rb +0 -139
  162. data/app/models/spree/promotion/actions/free_shipping.rb +0 -59
  163. data/app/models/spree/promotion/order_adjustments_recalculator.rb +0 -92
  164. data/app/models/spree/promotion/rules/first_order.rb +0 -38
  165. data/app/models/spree/promotion/rules/first_repeat_purchase_since.rb +0 -36
  166. data/app/models/spree/promotion/rules/item_total.rb +0 -86
  167. data/app/models/spree/promotion/rules/minimum_quantity.rb +0 -59
  168. data/app/models/spree/promotion/rules/nth_order.rb +0 -45
  169. data/app/models/spree/promotion/rules/one_use_per_user.rb +0 -25
  170. data/app/models/spree/promotion/rules/option_value.rb +0 -50
  171. data/app/models/spree/promotion/rules/product.rb +0 -86
  172. data/app/models/spree/promotion/rules/store.rb +0 -26
  173. data/app/models/spree/promotion/rules/taxon.rb +0 -91
  174. data/app/models/spree/promotion/rules/user.rb +0 -34
  175. data/app/models/spree/promotion/rules/user_logged_in.rb +0 -20
  176. data/app/models/spree/promotion/rules/user_role.rb +0 -45
  177. data/app/models/spree/promotion.rb +0 -271
  178. data/app/models/spree/promotion_action.rb +0 -47
  179. data/app/models/spree/promotion_category.rb +0 -8
  180. data/app/models/spree/promotion_chooser.rb +0 -34
  181. data/app/models/spree/promotion_code/batch_builder.rb +0 -64
  182. data/app/models/spree/promotion_code.rb +0 -54
  183. data/app/models/spree/promotion_code_batch.rb +0 -27
  184. data/app/models/spree/promotion_handler/cart.rb +0 -75
  185. data/app/models/spree/promotion_handler/coupon.rb +0 -123
  186. data/app/models/spree/promotion_handler/page.rb +0 -26
  187. data/app/models/spree/promotion_handler/shipping.rb +0 -61
  188. data/app/models/spree/promotion_rule.rb +0 -55
  189. data/app/models/spree/promotion_rule_store.rb +0 -10
  190. data/app/models/spree/promotion_rule_taxon.rb +0 -8
  191. data/app/models/spree/promotion_rule_user.rb +0 -10
  192. data/app/views/spree/promotion_code_batch_mailer/promotion_code_batch_errored.text.erb +0 -2
  193. data/app/views/spree/promotion_code_batch_mailer/promotion_code_batch_finished.text.erb +0 -2
  194. data/db/migrate/20161017102621_create_spree_promotion_code_batch.rb +0 -38
  195. data/db/migrate/20180202190713_create_promotion_rule_stores.rb +0 -12
  196. data/db/migrate/20180328172631_add_join_characters_to_promotion_code_batch.rb +0 -11
  197. data/db/migrate/20190106184413_remove_code_from_spree_promotions.rb +0 -42
  198. data/db/migrate/20220317165036_set_promotions_with_any_policy_to_all_if_possible.rb +0 -20
  199. data/db/migrate/20230322085416_remove_match_policy_from_spree_promotion.rb +0 -5
  200. data/db/migrate/20230325132905_remove_unused_columns_from_promotion_rules.rb +0 -6
  201. data/db/migrate/20230325161633_drop_unused_promo_action_line_items.rb +0 -13
  202. data/db/migrate/20231027084517_add_order_promotions_foreign_key.rb +0 -10
  203. data/db/migrate/20231031175215_add_promotion_order_promotion_foreign_key.rb +0 -10
  204. data/lib/solidus/migrations/promotions_with_code_handlers.rb +0 -66
  205. data/lib/spree/permission_sets/promotion_display.rb +0 -25
  206. data/lib/spree/permission_sets/promotion_management.rb +0 -25
  207. data/lib/spree/ransack_4_1_patch.rb +0 -16
  208. data/lib/spree/testing_support/factories/order_promotion_factory.rb +0 -8
  209. data/lib/spree/testing_support/factories/promotion_category_factory.rb +0 -7
  210. data/lib/spree/testing_support/factories/promotion_code_factory.rb +0 -8
  211. data/lib/spree/testing_support/factories/promotion_factory.rb +0 -98
@@ -1,52 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_dependency 'spree/calculator'
4
-
5
- module Spree
6
- class Calculator::TieredFlatRate < Calculator
7
- preference :base_amount, :decimal, default: 0
8
- preference :tiers, :hash, default: {}
9
- preference :currency, :string, default: -> { Spree::Config[:currency] }
10
-
11
- before_validation do
12
- # Convert tier values to decimals. Strings don't do us much good.
13
- if preferred_tiers.is_a?(Hash)
14
- self.preferred_tiers = preferred_tiers.map do |key, value|
15
- [cast_to_d(key.to_s), cast_to_d(value.to_s)]
16
- end.to_h
17
- end
18
- end
19
-
20
- validate :preferred_tiers_content
21
-
22
- def compute(object)
23
- _base, amount = preferred_tiers.sort.reverse.detect do |value, _|
24
- object.amount >= value
25
- end
26
-
27
- if preferred_currency.casecmp(object.currency).zero?
28
- amount || preferred_base_amount
29
- else
30
- 0
31
- end
32
- end
33
-
34
- private
35
-
36
- def cast_to_d(value)
37
- value.to_s.to_d
38
- rescue ArgumentError
39
- BigDecimal(0)
40
- end
41
-
42
- def preferred_tiers_content
43
- if preferred_tiers.is_a? Hash
44
- unless preferred_tiers.keys.all?{ |key| key.is_a?(Numeric) && key > 0 }
45
- errors.add(:base, :keys_should_be_positive_number)
46
- end
47
- else
48
- errors.add(:preferred_tiers, :should_be_hash)
49
- end
50
- end
51
- end
52
- end
@@ -1,62 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_dependency 'spree/calculator'
4
-
5
- module Spree
6
- class Calculator::TieredPercent < Calculator
7
- preference :base_percent, :decimal, default: 0
8
- preference :tiers, :hash, default: {}
9
- preference :currency, :string, default: -> { Spree::Config[:currency] }
10
-
11
- before_validation do
12
- # Convert tier values to decimals. Strings don't do us much good.
13
- if preferred_tiers.is_a?(Hash)
14
- self.preferred_tiers = preferred_tiers.map do |key, value|
15
- [cast_to_d(key.to_s), cast_to_d(value.to_s)]
16
- end.to_h
17
- end
18
- end
19
-
20
- validates :preferred_base_percent, numericality: {
21
- greater_than_or_equal_to: 0,
22
- less_than_or_equal_to: 100
23
- }
24
- validate :preferred_tiers_content
25
-
26
- def compute(object)
27
- order = object.is_a?(Order) ? object : object.order
28
-
29
- _base, percent = preferred_tiers.sort.reverse.detect do |value, _|
30
- order.item_total >= value
31
- end
32
-
33
- if preferred_currency.casecmp(order.currency).zero?
34
- currency_exponent = ::Money::Currency.find(preferred_currency).exponent
35
- (object.amount * (percent || preferred_base_percent) / 100).round(currency_exponent)
36
- else
37
- 0
38
- end
39
- end
40
-
41
- private
42
-
43
- def cast_to_d(value)
44
- value.to_s.to_d
45
- rescue ArgumentError
46
- BigDecimal(0)
47
- end
48
-
49
- def preferred_tiers_content
50
- if preferred_tiers.is_a? Hash
51
- unless preferred_tiers.keys.all?{ |key| key.is_a?(Numeric) && key > 0 }
52
- errors.add(:base, :keys_should_be_positive_number)
53
- end
54
- unless preferred_tiers.values.all?{ |key| key.is_a?(Numeric) && key >= 0 && key <= 100 }
55
- errors.add(:base, :values_should_be_percent)
56
- end
57
- else
58
- errors.add(:preferred_tiers, :should_be_hash)
59
- end
60
- end
61
- end
62
- end
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Spree
4
- # Spree::OrderPromotion represents the relationship between:
5
- #
6
- # 1. A promotion that a user attempted to apply to their order
7
- # 2. The specific code that they used
8
- class OrderPromotion < Spree::Base
9
- self.table_name = 'spree_orders_promotions'
10
-
11
- belongs_to :order, class_name: 'Spree::Order', optional: true
12
- belongs_to :promotion, class_name: 'Spree::Promotion', optional: true
13
- belongs_to :promotion_code, class_name: 'Spree::PromotionCode', optional: true
14
-
15
- validates :order, presence: true
16
- validates :promotion, presence: true
17
- validates :promotion_code, presence: true, if: :require_promotion_code?
18
-
19
- self.allowed_ransackable_associations = %w[promotion_code]
20
-
21
- private
22
-
23
- def require_promotion_code?
24
- promotion && !promotion.apply_automatically && promotion.codes.any?
25
- end
26
- end
27
- end
@@ -1,81 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Spree
4
- class Promotion < Spree::Base
5
- module Actions
6
- class CreateAdjustment < PromotionAction
7
- include Spree::CalculatedAdjustments
8
- include Spree::AdjustmentSource
9
-
10
- has_many :adjustments, as: :source
11
-
12
- delegate :eligible?, to: :promotion
13
-
14
- before_validation :ensure_action_has_calculator
15
- before_destroy :remove_adjustments_from_incomplete_orders
16
- before_discard :remove_adjustments_from_incomplete_orders
17
-
18
- def preload_relations
19
- [:calculator]
20
- end
21
-
22
- # Creates the adjustment related to a promotion for the order passed
23
- # through options hash
24
- #
25
- # Returns `true` if an adjustment is applied to an order,
26
- # `false` if the promotion has already been applied.
27
- def perform(options = {})
28
- order = options[:order]
29
- return if promotion_credit_exists?(order)
30
-
31
- amount = compute_amount(order)
32
- order.adjustments.create!(
33
- amount: amount,
34
- order: order,
35
- source: self,
36
- promotion_code: options[:promotion_code],
37
- label: I18n.t('spree.adjustment_labels.order', promotion: Spree::Promotion.model_name.human, promotion_name: promotion.name)
38
- )
39
- true
40
- end
41
-
42
- # Ensure a negative amount which does not exceed the sum of the order's
43
- # item_total and ship_total
44
- def compute_amount(calculable)
45
- amount = calculator.compute(calculable)
46
- amount ||= BigDecimal(0)
47
- amount = amount.abs
48
- [(calculable.item_total + calculable.ship_total), amount].min * -1
49
- end
50
-
51
- # Removes any adjustments generated by this action from the order.
52
- # @param order [Spree::Order] the order to remove the action from.
53
- # @return [void]
54
- def remove_from(order)
55
- order.adjustments.each do |adjustment|
56
- if adjustment.source == self
57
- order.adjustments.destroy(adjustment)
58
- end
59
- end
60
- end
61
-
62
- private
63
-
64
- # Tells us if there if the specified promotion is already associated with the line item
65
- # regardless of whether or not its currently eligible. Useful because generally
66
- # you would only want a promotion action to apply to order no more than once.
67
- #
68
- # Receives an adjustment +source+ (here a PromotionAction object) and tells
69
- # if the order has adjustments from that already
70
- def promotion_credit_exists?(adjustable)
71
- adjustments.where(adjustable_id: adjustable.id).exists?
72
- end
73
-
74
- def ensure_action_has_calculator
75
- return if calculator
76
- self.calculator = Calculator::FlatPercentItemTotal.new
77
- end
78
- end
79
- end
80
- end
81
- end
@@ -1,98 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Spree
4
- class Promotion < Spree::Base
5
- module Actions
6
- class CreateItemAdjustments < PromotionAction
7
- include Spree::CalculatedAdjustments
8
- include Spree::AdjustmentSource
9
-
10
- has_many :adjustments, as: :source
11
-
12
- delegate :eligible?, to: :promotion
13
-
14
- before_validation :ensure_action_has_calculator
15
- before_destroy :remove_adjustments_from_incomplete_orders
16
- before_discard :remove_adjustments_from_incomplete_orders
17
-
18
- def preload_relations
19
- [:calculator]
20
- end
21
-
22
- def perform(payload = {})
23
- order = payload[:order]
24
- promotion = payload[:promotion]
25
- promotion_code = payload[:promotion_code]
26
-
27
- results = line_items_to_adjust(promotion, order).map do |line_item|
28
- create_adjustment(line_item, order, promotion_code)
29
- end
30
-
31
- results.any?
32
- end
33
-
34
- # Ensure a negative amount which does not exceed the sum of the order's
35
- # item_total and ship_total
36
- def compute_amount(adjustable)
37
- order = adjustable.is_a?(Order) ? adjustable : adjustable.order
38
- return 0 unless promotion.line_item_actionable?(order, adjustable)
39
- promotion_amount = calculator.compute(adjustable)
40
- promotion_amount ||= BigDecimal(0)
41
- promotion_amount = promotion_amount.abs
42
- [adjustable.amount, promotion_amount].min * -1
43
- end
44
-
45
- # Removes any adjustments generated by this action from the order's
46
- # line items.
47
- # @param order [Spree::Order] the order to remove the action from.
48
- # @return [void]
49
- def remove_from(order)
50
- order.line_items.each do |line_item|
51
- line_item.adjustments.each do |adjustment|
52
- if adjustment.source == self
53
- line_item.adjustments.destroy(adjustment)
54
- end
55
- end
56
- end
57
- end
58
-
59
- private
60
-
61
- def create_adjustment(adjustable, order, promotion_code)
62
- amount = compute_amount(adjustable)
63
- return if amount == 0
64
- adjustable.adjustments.create!(
65
- source: self,
66
- amount: amount,
67
- order: order,
68
- promotion_code: promotion_code,
69
- label: I18n.t('spree.adjustment_labels.line_item', promotion: Spree::Promotion.model_name.human, promotion_name: promotion.name)
70
- )
71
- true
72
- end
73
-
74
- # Tells us if there if the specified promotion is already associated with the line item
75
- # regardless of whether or not its currently eligible. Useful because generally
76
- # you would only want a promotion action to apply to line item no more than once.
77
- #
78
- # Receives an adjustment +source+ (here a PromotionAction object) and tells
79
- # if the order has adjustments from that already
80
- def promotion_credit_exists?(adjustable)
81
- adjustments.where(adjustable_id: adjustable.id).exists?
82
- end
83
-
84
- def ensure_action_has_calculator
85
- return if calculator
86
- self.calculator = Calculator::PercentOnLineItem.new
87
- end
88
-
89
- def line_items_to_adjust(promotion, order)
90
- order.line_items.select do |line_item|
91
- line_item.adjustments.none? { |adjustment| adjustment.source == self } &&
92
- promotion.line_item_actionable?(order, line_item)
93
- end
94
- end
95
- end
96
- end
97
- end
98
- end
@@ -1,139 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Spree
4
- class Promotion < Spree::Base
5
- module Actions
6
- class CreateQuantityAdjustments < CreateItemAdjustments
7
- preference :group_size, :integer, default: 1
8
-
9
- has_many :line_item_actions, foreign_key: :action_id, dependent: :destroy
10
- has_many :line_items, through: :line_item_actions
11
-
12
- ##
13
- # Computes the amount for the adjustment based on the line item and any
14
- # other applicable items in the order. The rules for this specific
15
- # adjustment are as follows:
16
- #
17
- # = Setup
18
- #
19
- # We have a quantity group promotion on t-shirts. If a user orders 3
20
- # t-shirts, they get $5 off of each. The shirts come in one size and three
21
- # colours: red, blue, and white.
22
- #
23
- # == Scenario 1
24
- #
25
- # User has 2 red shirts, 1 white shirt, and 1 blue shirt in their
26
- # order. We want to compute the adjustment amount for the white shirt.
27
- #
28
- # *Result:* -$5
29
- #
30
- # *Reasoning:* There are a total of 4 items that are eligible for the
31
- # promotion. Since that is greater than 3, we can discount the items. The
32
- # white shirt has a quantity of 1, therefore it will get discounted by
33
- # +adjustment_amount * 1+ or $5.
34
- #
35
- # === Scenario 1-1
36
- #
37
- # What about the blue shirt? How much does it get discounted?
38
- #
39
- # *Result:* $0
40
- #
41
- # *Reasoning:* We have a total quantity of 4. However, we only apply the
42
- # adjustment to groups of 3. Assuming the white and red shirts have already
43
- # had their adjustment calculated, that means 3 units have been discounted.
44
- # Leaving us with a lonely blue shirt that isn't part of a group of 3.
45
- # Therefore, it does not receive the discount.
46
- #
47
- # == Scenario 2
48
- #
49
- # User has 4 red shirts in their order. What is the amount?
50
- #
51
- # *Result:* -$15
52
- #
53
- # *Reasoning:* The total quantity of eligible items is 4, so we the
54
- # adjustment will be non-zero. However, we only apply it to groups of 3,
55
- # therefore there is one extra item that is not eligible for the
56
- # adjustment. +adjustment_amount * 3+ or $15.
57
- #
58
- def compute_amount(line_item)
59
- adjustment_amount = calculator.compute(PartialLineItem.new(line_item))
60
- adjustment_amount ||= BigDecimal(0)
61
- adjustment_amount = adjustment_amount.abs
62
-
63
- order = line_item.order
64
- line_items = actionable_line_items(order)
65
-
66
- actioned_line_items = order.line_item_adjustments.reload.
67
- select { |adjustment| adjustment.source == self && adjustment.amount < 0 }.
68
- map(&:adjustable)
69
- other_line_items = actioned_line_items - [line_item]
70
-
71
- applicable_quantity = total_applicable_quantity(line_items)
72
- used_quantity = total_used_quantity(other_line_items)
73
- usable_quantity = [
74
- applicable_quantity - used_quantity,
75
- line_item.quantity
76
- ].min
77
-
78
- persist_quantity(usable_quantity, line_item)
79
-
80
- amount = adjustment_amount * usable_quantity
81
- [line_item.amount, amount].min * -1
82
- end
83
-
84
- private
85
-
86
- def actionable_line_items(order)
87
- order.line_items.select do |item|
88
- promotion.line_item_actionable? order, item
89
- end
90
- end
91
-
92
- def total_applicable_quantity(line_items)
93
- total_quantity = line_items.sum(&:quantity)
94
- extra_quantity = total_quantity % preferred_group_size
95
-
96
- total_quantity - extra_quantity
97
- end
98
-
99
- def total_used_quantity(line_items)
100
- line_item_actions.where(
101
- line_item_id: line_items.map(&:id)
102
- ).sum(:quantity)
103
- end
104
-
105
- def persist_quantity(quantity, line_item)
106
- line_item_action = line_item_actions.where(
107
- line_item_id: line_item.id
108
- ).first_or_initialize
109
- line_item_action.quantity = quantity
110
- line_item_action.save!
111
- end
112
-
113
- ##
114
- # Used specifically for PercentOnLineItem calculator. That calculator uses
115
- # `line_item.amount`, however we might not necessarily want to discount the
116
- # entire amount. This class allows us to determine the discount per
117
- # quantity and then calculate the adjustment amount the way we normally do
118
- # for flat rate adjustments.
119
- class PartialLineItem
120
- def initialize(line_item)
121
- @line_item = line_item
122
- end
123
-
124
- def amount
125
- @line_item.price
126
- end
127
-
128
- def order
129
- @line_item.order
130
- end
131
-
132
- def currency
133
- @line_item.currency
134
- end
135
- end
136
- end
137
- end
138
- end
139
- end
@@ -1,59 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Spree
4
- class Promotion < Spree::Base
5
- module Actions
6
- class FreeShipping < Spree::PromotionAction
7
- def perform(payload = {})
8
- order = payload[:order]
9
- promotion_code = payload[:promotion_code]
10
- return false unless promotion.eligible? order
11
-
12
- created_adjustments = order.shipments.map do |shipment|
13
- next if promotion_credit_exists?(shipment)
14
-
15
- shipment.adjustments.create!(
16
- order: shipment.order,
17
- amount: compute_amount(shipment),
18
- source: self,
19
- promotion_code: promotion_code,
20
- label: label
21
- )
22
- end
23
-
24
- # Did we actually end up creating any adjustments?
25
- # If so, then this action should be classed as 'successful'
26
- created_adjustments.any?
27
- end
28
-
29
- def label
30
- "#{I18n.t('spree.promotion')} (#{promotion.name})"
31
- end
32
-
33
- def compute_amount(shipment)
34
- shipment.cost * -1
35
- end
36
-
37
- # Removes any adjustments generated by this action from the order's
38
- # shipments.
39
- # @param order [Spree::Order] the order to remove the action from.
40
- # @return [void]
41
- def remove_from(order)
42
- order.shipments.each do |shipment|
43
- shipment.adjustments.each do |adjustment|
44
- if adjustment.source == self
45
- shipment.adjustments.destroy(adjustment)
46
- end
47
- end
48
- end
49
- end
50
-
51
- private
52
-
53
- def promotion_credit_exists?(shipment)
54
- shipment.adjustments.where(source: self).exists?
55
- end
56
- end
57
- end
58
- end
59
- end
@@ -1,92 +0,0 @@
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
@@ -1,38 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Spree
4
- class Promotion < Spree::Base
5
- module Rules
6
- class FirstOrder < PromotionRule
7
- attr_reader :user, :email
8
-
9
- def applicable?(promotable)
10
- promotable.is_a?(Spree::Order)
11
- end
12
-
13
- def eligible?(order, options = {})
14
- @user = order.try(:user) || options[:user]
15
- @email = order.email
16
-
17
- if user || email
18
- if !completed_orders.blank? && completed_orders.first != order
19
- eligibility_errors.add(:base, eligibility_error_message(:not_first_order), error_code: :not_first_order)
20
- end
21
- end
22
-
23
- eligibility_errors.empty?
24
- end
25
-
26
- private
27
-
28
- def completed_orders
29
- user ? user.orders.complete : orders_by_email
30
- end
31
-
32
- def orders_by_email
33
- Spree::Order.where(email: email).complete
34
- end
35
- end
36
- end
37
- end
38
- end
@@ -1,36 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Spree
4
- class Promotion < Spree::Base
5
- module Rules
6
- class FirstRepeatPurchaseSince < PromotionRule
7
- preference :days_ago, :integer, default: 365
8
- validates :preferred_days_ago, numericality: { only_integer: true, greater_than: 0 }
9
-
10
- # This promotion is applicable to orders only.
11
- def applicable?(promotable)
12
- promotable.is_a?(Spree::Order)
13
- end
14
-
15
- # This is never eligible if the order does not have a user, and that user does not have any previous completed orders.
16
- #
17
- # This is eligible if the user's most recently completed order is more than the preferred days ago
18
- # @param order [Spree::Order]
19
- def eligible?(order, _options = {})
20
- return false unless order.user
21
-
22
- last_order = last_completed_order(order.user)
23
- return false unless last_order
24
-
25
- last_order.completed_at < preferred_days_ago.days.ago
26
- end
27
-
28
- private
29
-
30
- def last_completed_order(user)
31
- user.orders.complete.order(:completed_at).last
32
- end
33
- end
34
- end
35
- end
36
- end