solidus_core 4.3.4 → 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 +17 -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,271 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Spree
4
- class Promotion < Spree::Base
5
- UNACTIVATABLE_ORDER_STATES = ["complete", "awaiting_return", "returned"]
6
-
7
- attr_reader :eligibility_errors
8
-
9
- belongs_to :promotion_category, optional: true
10
-
11
- has_many :promotion_rules, autosave: true, dependent: :destroy, inverse_of: :promotion
12
- alias_method :rules, :promotion_rules
13
-
14
- has_many :promotion_actions, autosave: true, dependent: :destroy, inverse_of: :promotion
15
- alias_method :actions, :promotion_actions
16
-
17
- has_many :order_promotions, class_name: "Spree::OrderPromotion", inverse_of: :promotion, dependent: :destroy
18
- has_many :orders, through: :order_promotions
19
-
20
- has_many :codes, class_name: "Spree::PromotionCode", inverse_of: :promotion, dependent: :destroy
21
- alias_method :promotion_codes, :codes
22
-
23
- has_many :promotion_code_batches, class_name: "Spree::PromotionCodeBatch", dependent: :destroy
24
-
25
- accepts_nested_attributes_for :promotion_actions, :promotion_rules
26
-
27
- validates_associated :rules
28
-
29
- validates :name, presence: true
30
- validates :path, uniqueness: { allow_blank: true, case_sensitive: true }
31
- validates :usage_limit, numericality: { greater_than: 0, allow_nil: true }
32
- validates :per_code_usage_limit, numericality: { greater_than_or_equal_to: 0, allow_nil: true }
33
- validates :description, length: { maximum: 255 }
34
- validate :apply_automatically_disallowed_with_paths
35
-
36
- before_save :normalize_blank_values
37
-
38
- scope :coupons, -> { joins(:codes).distinct }
39
- scope :advertised, -> { where(advertise: true) }
40
- scope :active, -> { has_actions.started_and_unexpired }
41
- scope :started_and_unexpired, -> do
42
- table = arel_table
43
- time = Time.current
44
-
45
- where(table[:starts_at].eq(nil).or(table[:starts_at].lt(time))).
46
- where(table[:expires_at].eq(nil).or(table[:expires_at].gt(time)))
47
- end
48
- scope :has_actions, -> do
49
- joins(:promotion_actions).distinct
50
- end
51
- scope :applied, -> { joins(:order_promotions).distinct }
52
-
53
- self.allowed_ransackable_associations = ['codes']
54
- self.allowed_ransackable_attributes = %w[name path promotion_category_id]
55
- self.allowed_ransackable_scopes = %i[active]
56
-
57
- def self.order_activatable?(order)
58
- order && !UNACTIVATABLE_ORDER_STATES.include?(order.state)
59
- end
60
-
61
- def self.with_coupon_code(val)
62
- joins(:codes).where(
63
- PromotionCode.arel_table[:value].eq(val.downcase)
64
- ).first
65
- end
66
-
67
- # All orders that have been discounted using this promotion
68
- def discounted_orders
69
- Spree::Order.
70
- joins(:all_adjustments).
71
- where(
72
- spree_adjustments: {
73
- source_type: "Spree::PromotionAction",
74
- source_id: actions.map(&:id),
75
- eligible: true
76
- }
77
- ).distinct
78
- end
79
-
80
- def as_json(options = {})
81
- options[:except] ||= :code
82
- super
83
- end
84
-
85
- def not_started?
86
- !started?
87
- end
88
-
89
- def started?
90
- starts_at.nil? || starts_at < Time.current
91
- end
92
-
93
- def expired?
94
- expires_at.present? && expires_at < Time.current
95
- end
96
-
97
- def not_expired?
98
- !expired?
99
- end
100
-
101
- def active?
102
- started? && not_expired? && actions.present?
103
- end
104
-
105
- def inactive?
106
- !active?
107
- end
108
-
109
- def activate(order:, line_item: nil, user: nil, path: nil, promotion_code: nil)
110
- return unless self.class.order_activatable?(order)
111
-
112
- payload = {
113
- order: order,
114
- promotion: self,
115
- line_item: line_item,
116
- user: user,
117
- path: path,
118
- promotion_code: promotion_code
119
- }
120
-
121
- # Track results from actions to see if any action has been taken.
122
- # Actions should return nil/false if no action has been taken.
123
- # If an action returns true, then an action has been taken.
124
- results = actions.map do |action|
125
- action.perform(payload)
126
- end
127
- # If an action has been taken, report back to whatever activated this promotion.
128
- action_taken = results.include?(true)
129
-
130
- if action_taken
131
- # connect to the order
132
- order.order_promotions.find_or_create_by!(
133
- promotion: self,
134
- promotion_code: promotion_code,
135
- )
136
- order.promotions.reset
137
- order_promotions.reset
138
- orders.reset
139
- end
140
-
141
- action_taken
142
- end
143
-
144
- # called anytime order.recalculate happens
145
- def eligible?(promotable, promotion_code: nil)
146
- return false if inactive?
147
- return false if blacklisted?(promotable)
148
-
149
- excluded_orders = eligibility_excluded_orders(promotable)
150
- return false if usage_limit_exceeded?(excluded_orders: excluded_orders)
151
- return false if promotion_code&.usage_limit_exceeded?(excluded_orders: excluded_orders)
152
-
153
- !!eligible_rules(promotable, {})
154
- end
155
-
156
- # eligible_rules returns an array of promotion rules where eligible? is true for the promotable
157
- # if there are no such rules, an empty array is returned
158
- # if the rules make this promotable ineligible, then nil is returned (i.e. this promotable is not eligible)
159
- def eligible_rules(promotable, options = {})
160
- # Promotions without rules are eligible by default.
161
- return [] if rules.none?
162
-
163
- eligible = lambda { |rule| rule.eligible?(promotable, options) }
164
- specific_rules = rules.select { |rule| rule.applicable?(promotable) }
165
- return [] if specific_rules.none?
166
-
167
- # If there are rules for this promotion, but no rules for this
168
- # particular promotable, then the promotion is ineligible by default.
169
- unless specific_rules.all?(&eligible)
170
- @eligibility_errors = specific_rules.map(&:eligibility_errors).detect(&:present?)
171
- return nil
172
- end
173
- specific_rules
174
- end
175
-
176
- def products
177
- rules.where(type: "Spree::Promotion::Rules::Product").flat_map(&:products).uniq
178
- end
179
-
180
- # Whether the promotion has exceeded its usage restrictions.
181
- #
182
- # @param excluded_orders [Array<Spree::Order>] Orders to exclude from usage limit
183
- # @return true or false
184
- def usage_limit_exceeded?(excluded_orders: [])
185
- if usage_limit
186
- usage_count(excluded_orders: excluded_orders) >= usage_limit
187
- end
188
- end
189
-
190
- # Number of times the code has been used overall
191
- #
192
- # @param excluded_orders [Array<Spree::Order>] Orders to exclude from usage count
193
- # @return [Integer] usage count
194
- def usage_count(excluded_orders: [])
195
- discounted_orders.
196
- complete.
197
- where.not(id: [excluded_orders.map(&:id)]).
198
- where.not(spree_orders: { state: :canceled }).
199
- count
200
- end
201
-
202
- def line_item_actionable?(order, line_item, promotion_code: nil)
203
- return false if blacklisted?(line_item)
204
-
205
- if eligible?(order, promotion_code: promotion_code)
206
- rules = eligible_rules(order)
207
- if rules.blank?
208
- true
209
- else
210
- rules.all? { |rule| rule.actionable? line_item }
211
- end
212
- else
213
- false
214
- end
215
- end
216
-
217
- def used_by?(user, excluded_orders = [])
218
- discounted_orders.
219
- complete.
220
- where.not(id: excluded_orders.map(&:id)).
221
- where(user: user).
222
- where.not(spree_orders: { state: :canceled }).
223
- exists?
224
- end
225
-
226
- # Removes a promotion and any adjustments or other side effects from an
227
- # order.
228
- # @param order [Spree::Order] the order to remove the promotion from.
229
- # @return [void]
230
- def remove_from(order)
231
- actions.each do |action|
232
- action.remove_from(order)
233
- end
234
- # note: this destroys the join table entry, not the promotion itself
235
- order.promotions.destroy(self)
236
- order.order_promotions.reset
237
- order_promotions.reset
238
- end
239
-
240
- private
241
-
242
- def blacklisted?(promotable)
243
- case promotable
244
- when Spree::LineItem
245
- !promotable.variant.product.promotionable?
246
- when Spree::Order
247
- promotable.line_items.any? { |line_item| !line_item.variant.product.promotionable? }
248
- end
249
- end
250
-
251
- def normalize_blank_values
252
- self[:path] = nil if self[:path].blank?
253
- end
254
-
255
- def apply_automatically_disallowed_with_paths
256
- return unless apply_automatically
257
-
258
- errors.add(:apply_automatically, :disallowed_with_path) if path.present?
259
- end
260
-
261
- def eligibility_excluded_orders(promotable)
262
- if promotable.is_a?(Spree::Order)
263
- [promotable]
264
- elsif promotable.respond_to?(:order)
265
- [promotable.order]
266
- else
267
- []
268
- end
269
- end
270
- end
271
- end
@@ -1,47 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spree/preferences/persistable'
4
-
5
- module Spree
6
- # Base class for all types of promotion action.
7
- #
8
- # PromotionActions perform the necessary tasks when a promotion is activated
9
- # by an event and determined to be eligible.
10
- class PromotionAction < Spree::Base
11
- include Spree::Preferences::Persistable
12
- include Spree::SoftDeletable
13
-
14
- belongs_to :promotion, class_name: 'Spree::Promotion', inverse_of: :promotion_actions, optional: true
15
-
16
- scope :of_type, ->(type) { where(type: Array.wrap(type).map(&:to_s)) }
17
- scope :shipping, -> { of_type(Spree::Config.environment.promotions.shipping_actions.to_a) }
18
-
19
- def preload_relations
20
- []
21
- end
22
-
23
- # Updates the state of the order or performs some other action depending on
24
- # the subclass options will contain the payload from the event that
25
- # activated the promotion. This will include the key :user which allows
26
- # user based actions to be performed in addition to actions on the order
27
- #
28
- # @note This method should be overriden in subclassses.
29
- def perform(_options = {})
30
- raise 'perform should be implemented in a sub-class of PromotionAction'
31
- end
32
-
33
- # Removes the action from an order
34
- #
35
- # @note This method should be overriden in subclassses.
36
- #
37
- # @param order [Spree::Order] the order to remove the action from
38
- # @return [void]
39
- def remove_from(_order)
40
- raise 'remove_from should be implemented in a sub-class of PromotionAction'
41
- end
42
-
43
- def to_partial_path
44
- "spree/admin/promotions/actions/#{model_name.element}"
45
- end
46
- end
47
- end
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Spree
4
- class PromotionCategory < Spree::Base
5
- validates_presence_of :name
6
- has_many :promotions
7
- end
8
- end
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Spree
4
- class PromotionChooser
5
- def initialize(adjustments)
6
- @adjustments = adjustments
7
- end
8
-
9
- # Picks the best promotion from this set of adjustments, all others are
10
- # marked as ineligible.
11
- #
12
- # @return [BigDecimal] The amount of the best adjustment
13
- def update
14
- if best_promotion_adjustment
15
- @adjustments.select(&:eligible?).each do |adjustment|
16
- next if adjustment == best_promotion_adjustment
17
- adjustment.update_columns(eligible: false, updated_at: Time.current)
18
- end
19
- best_promotion_adjustment.amount
20
- else
21
- BigDecimal('0')
22
- end
23
- end
24
-
25
- private
26
-
27
- # @return The best promotion from this set of adjustments.
28
- def best_promotion_adjustment
29
- @best_promotion_adjustment ||= @adjustments.select(&:eligible?).min_by do |adjustment|
30
- [adjustment.amount, -adjustment.id]
31
- end
32
- end
33
- end
34
- end
@@ -1,64 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class ::Spree::PromotionCode::BatchBuilder
4
- attr_reader :promotion_code_batch, :options
5
- delegate :promotion, :number_of_codes, :base_code, to: :promotion_code_batch
6
-
7
- DEFAULT_OPTIONS = {
8
- random_code_length: 6,
9
- batch_size: 1000,
10
- sample_characters: ('a'..'z').to_a + (2..9).to_a.map(&:to_s)
11
- }
12
-
13
- def initialize(promotion_code_batch, options = {})
14
- @promotion_code_batch = promotion_code_batch
15
- options.assert_valid_keys(*DEFAULT_OPTIONS.keys)
16
- @options = DEFAULT_OPTIONS.merge(options)
17
- end
18
-
19
- def build_promotion_codes
20
- generate_random_codes
21
- promotion_code_batch.update!(state: "completed")
22
- rescue StandardError => error
23
- promotion_code_batch.update!(
24
- error: error.inspect,
25
- state: "failed"
26
- )
27
- raise error
28
- end
29
-
30
- private
31
-
32
- def generate_random_codes
33
- created_codes = 0
34
- batch_size = @options[:batch_size]
35
-
36
- while created_codes < number_of_codes
37
- max_codes_to_generate = [batch_size, number_of_codes - created_codes].min
38
-
39
- new_codes = Array.new(max_codes_to_generate) { generate_random_code }.uniq
40
- codes_for_current_batch = get_unique_codes(new_codes)
41
-
42
- codes_for_current_batch.each do |value|
43
- Spree::PromotionCode.create!(
44
- value: value,
45
- promotion: promotion,
46
- promotion_code_batch: promotion_code_batch
47
- )
48
- end
49
- created_codes += codes_for_current_batch.size
50
- end
51
- end
52
-
53
- def generate_random_code
54
- suffix = Array.new(@options[:random_code_length]) do
55
- @options[:sample_characters].sample
56
- end.join
57
-
58
- "#{base_code}#{@promotion_code_batch.join_characters}#{suffix}"
59
- end
60
-
61
- def get_unique_codes(code_set)
62
- code_set - Spree::PromotionCode.where(value: code_set.to_a).pluck(:value)
63
- end
64
- end
@@ -1,54 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Spree::PromotionCode < Spree::Base
4
- belongs_to :promotion, inverse_of: :codes, optional: true
5
- belongs_to :promotion_code_batch, class_name: "Spree::PromotionCodeBatch", optional: true
6
- has_many :adjustments
7
-
8
- before_validation :normalize_code
9
-
10
- validates :value, presence: true, uniqueness: { allow_blank: true, case_sensitive: true }
11
- validates :promotion, presence: true
12
- validate :promotion_not_apply_automatically, on: :create
13
-
14
- self.allowed_ransackable_attributes = ['value']
15
-
16
- # Whether the promotion code has exceeded its usage restrictions
17
- #
18
- # @param excluded_orders [Array<Spree::Order>] Orders to exclude from usage limit
19
- # @return true or false
20
- def usage_limit_exceeded?(excluded_orders: [])
21
- if usage_limit
22
- usage_count(excluded_orders: excluded_orders) >= usage_limit
23
- end
24
- end
25
-
26
- # Number of times the code has been used overall
27
- #
28
- # @param excluded_orders [Array<Spree::Order>] Orders to exclude from usage count
29
- # @return [Integer] usage count
30
- def usage_count(excluded_orders: [])
31
- promotion.
32
- discounted_orders.
33
- complete.
34
- where.not(spree_orders: { state: :canceled }).
35
- joins(:order_promotions).
36
- where(spree_orders_promotions: { promotion_code_id: self.id }).
37
- where.not(id: excluded_orders.map(&:id)).
38
- count
39
- end
40
-
41
- def usage_limit
42
- promotion.per_code_usage_limit
43
- end
44
-
45
- def promotion_not_apply_automatically
46
- errors.add(:base, :disallowed_with_apply_automatically) if promotion.apply_automatically
47
- end
48
-
49
- private
50
-
51
- def normalize_code
52
- self.value = value.downcase.strip
53
- end
54
- end
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Spree
4
- class PromotionCodeBatch < Spree::Base
5
- class CantProcessStartedBatch < StandardError
6
- end
7
-
8
- belongs_to :promotion, class_name: "Spree::Promotion", optional: true
9
- has_many :promotion_codes, class_name: "Spree::PromotionCode", dependent: :destroy
10
-
11
- validates :number_of_codes, numericality: { greater_than: 0 }
12
- validates_presence_of :base_code, :number_of_codes
13
-
14
- def finished?
15
- state == "completed"
16
- end
17
-
18
- def process
19
- if state == "pending"
20
- update!(state: "processing")
21
- PromotionCodeBatchJob.perform_later(self)
22
- else
23
- raise CantProcessStartedBatch.new("Batch #{id} already started")
24
- end
25
- end
26
- end
27
- end
@@ -1,75 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Spree
4
- module PromotionHandler
5
- # Decides which promotion should be activated given the current order context
6
- #
7
- # By activated it doesn't necessarily mean that the order will have a
8
- # discount for every activated promotion. It means that the discount will be
9
- # created and might eventually become eligible. The intention here is to
10
- # reduce overhead. e.g. a promotion that requires item A to be eligible
11
- # shouldn't be eligible unless item A is added to the order.
12
- #
13
- # It can be used as a wrapper for custom handlers as well. Different
14
- # applications might have completely different requirements to make
15
- # the promotions system accurate and performant. Here they can plug custom
16
- # handler to activate promos as they wish once an item is added to cart
17
- class Cart
18
- attr_reader :line_item, :order
19
- attr_accessor :error, :success
20
-
21
- def initialize(order, line_item = nil)
22
- @order, @line_item = order, line_item
23
- end
24
-
25
- def activate
26
- promotions.each do |promotion|
27
- if (line_item && promotion.eligible?(line_item, promotion_code: promotion_code(promotion))) || promotion.eligible?(order, promotion_code: promotion_code(promotion))
28
- promotion.activate(line_item: line_item, order: order, promotion_code: promotion_code(promotion))
29
- end
30
- end
31
- end
32
-
33
- private
34
-
35
- def promotions
36
- promos = connected_order_promotions | sale_promotions
37
- promos.flat_map(&:promotion_actions).group_by(&:preload_relations).each do |preload_relations, actions|
38
- preload(records: actions, associations: preload_relations)
39
- end
40
- promos.flat_map(&:promotion_rules).group_by(&:preload_relations).each do |preload_relations, rules|
41
- preload(records: rules, associations: preload_relations)
42
- end
43
- promos
44
- end
45
-
46
- def preload(records:, associations:)
47
- if Rails::VERSION::MAJOR >= 7
48
- ActiveRecord::Associations::Preloader.new(records: records, associations: associations).call
49
- else
50
- ActiveRecord::Associations::Preloader.new.preload(records, associations)
51
- end
52
- end
53
-
54
- def connected_order_promotions
55
- order.promotions.active.includes(promotion_includes)
56
- end
57
-
58
- def sale_promotions
59
- Spree::Promotion.where(apply_automatically: true).active.includes(promotion_includes)
60
- end
61
-
62
- def promotion_code(promotion)
63
- order_promotion = order.order_promotions.detect { |op| op.promotion_id == promotion.id }
64
- order_promotion.present? ? order_promotion.promotion_code : nil
65
- end
66
-
67
- def promotion_includes
68
- [
69
- :promotion_rules,
70
- :promotion_actions,
71
- ]
72
- end
73
- end
74
- end
75
- end