solidus_core 4.3.5 → 4.4.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 (213) 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 -4
  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 -127
  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 +7 -7
  149. metadata +60 -93
  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/line_item_action.rb +0 -8
  159. data/app/models/spree/order_promotion.rb +0 -27
  160. data/app/models/spree/promotion/actions/create_adjustment.rb +0 -81
  161. data/app/models/spree/promotion/actions/create_item_adjustments.rb +0 -98
  162. data/app/models/spree/promotion/actions/create_quantity_adjustments.rb +0 -139
  163. data/app/models/spree/promotion/actions/free_shipping.rb +0 -59
  164. data/app/models/spree/promotion/order_adjustments_recalculator.rb +0 -92
  165. data/app/models/spree/promotion/rules/first_order.rb +0 -38
  166. data/app/models/spree/promotion/rules/first_repeat_purchase_since.rb +0 -36
  167. data/app/models/spree/promotion/rules/item_total.rb +0 -86
  168. data/app/models/spree/promotion/rules/minimum_quantity.rb +0 -59
  169. data/app/models/spree/promotion/rules/nth_order.rb +0 -45
  170. data/app/models/spree/promotion/rules/one_use_per_user.rb +0 -25
  171. data/app/models/spree/promotion/rules/option_value.rb +0 -50
  172. data/app/models/spree/promotion/rules/product.rb +0 -86
  173. data/app/models/spree/promotion/rules/store.rb +0 -26
  174. data/app/models/spree/promotion/rules/taxon.rb +0 -91
  175. data/app/models/spree/promotion/rules/user.rb +0 -34
  176. data/app/models/spree/promotion/rules/user_logged_in.rb +0 -20
  177. data/app/models/spree/promotion/rules/user_role.rb +0 -45
  178. data/app/models/spree/promotion.rb +0 -271
  179. data/app/models/spree/promotion_action.rb +0 -47
  180. data/app/models/spree/promotion_category.rb +0 -8
  181. data/app/models/spree/promotion_chooser.rb +0 -34
  182. data/app/models/spree/promotion_code/batch_builder.rb +0 -64
  183. data/app/models/spree/promotion_code.rb +0 -54
  184. data/app/models/spree/promotion_code_batch.rb +0 -27
  185. data/app/models/spree/promotion_handler/cart.rb +0 -75
  186. data/app/models/spree/promotion_handler/coupon.rb +0 -123
  187. data/app/models/spree/promotion_handler/page.rb +0 -26
  188. data/app/models/spree/promotion_handler/shipping.rb +0 -61
  189. data/app/models/spree/promotion_rule.rb +0 -55
  190. data/app/models/spree/promotion_rule_store.rb +0 -10
  191. data/app/models/spree/promotion_rule_taxon.rb +0 -8
  192. data/app/models/spree/promotion_rule_user.rb +0 -10
  193. data/app/views/spree/promotion_code_batch_mailer/promotion_code_batch_errored.text.erb +0 -2
  194. data/app/views/spree/promotion_code_batch_mailer/promotion_code_batch_finished.text.erb +0 -2
  195. data/bin/rails +0 -13
  196. data/db/migrate/20161017102621_create_spree_promotion_code_batch.rb +0 -38
  197. data/db/migrate/20180202190713_create_promotion_rule_stores.rb +0 -12
  198. data/db/migrate/20180328172631_add_join_characters_to_promotion_code_batch.rb +0 -11
  199. data/db/migrate/20190106184413_remove_code_from_spree_promotions.rb +0 -42
  200. data/db/migrate/20220317165036_set_promotions_with_any_policy_to_all_if_possible.rb +0 -20
  201. data/db/migrate/20230322085416_remove_match_policy_from_spree_promotion.rb +0 -5
  202. data/db/migrate/20230325132905_remove_unused_columns_from_promotion_rules.rb +0 -6
  203. data/db/migrate/20230325161633_drop_unused_promo_action_line_items.rb +0 -13
  204. data/db/migrate/20231027084517_add_order_promotions_foreign_key.rb +0 -10
  205. data/db/migrate/20231031175215_add_promotion_order_promotion_foreign_key.rb +0 -10
  206. data/lib/solidus/migrations/promotions_with_code_handlers.rb +0 -66
  207. data/lib/spree/permission_sets/promotion_display.rb +0 -25
  208. data/lib/spree/permission_sets/promotion_management.rb +0 -25
  209. data/lib/spree/ransack_4_1_patch.rb +0 -16
  210. data/lib/spree/testing_support/factories/order_promotion_factory.rb +0 -8
  211. data/lib/spree/testing_support/factories/promotion_category_factory.rb +0 -7
  212. data/lib/spree/testing_support/factories/promotion_code_factory.rb +0 -8
  213. 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,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Spree
4
- class LineItemAction < Spree::Base
5
- belongs_to :line_item, optional: true
6
- belongs_to :action, class_name: "Spree::PromotionAction", optional: true
7
- end
8
- 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