solidus_core 4.3.4 → 4.4.0

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 (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