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.
- checksums.yaml +4 -4
- data/app/helpers/spree/base_helper.rb +2 -2
- data/app/mailers/spree/base_mailer.rb +1 -1
- data/app/mailers/spree/carton_mailer.rb +1 -1
- data/app/mailers/spree/order_mailer.rb +3 -3
- data/app/mailers/spree/reimbursement_mailer.rb +1 -1
- data/app/models/concerns/spree/active_storage_adapter/normalization.rb +1 -1
- data/app/models/concerns/spree/active_storage_adapter.rb +1 -1
- data/app/models/concerns/spree/display_money.rb +1 -1
- data/app/models/concerns/spree/ordered_property_value_list.rb +2 -2
- data/app/models/concerns/spree/user_address_book.rb +4 -4
- data/app/models/concerns/spree/user_methods.rb +3 -3
- data/app/models/spree/address.rb +6 -6
- data/app/models/spree/adjustment.rb +13 -66
- data/app/models/spree/calculator/returns/default_refund_amount.rb +1 -1
- data/app/models/spree/carton.rb +2 -2
- data/app/models/spree/credit_card.rb +6 -6
- data/app/models/spree/deprecated_configurable_class.rb +40 -0
- data/app/models/spree/fulfilment_changer.rb +4 -4
- data/app/models/spree/inventory_unit.rb +2 -2
- data/app/models/spree/line_item.rb +1 -1
- data/app/models/spree/null_promotion_adjuster.rb +13 -0
- data/app/models/spree/null_promotion_advertiser.rb +9 -0
- data/app/models/spree/null_promotion_finder.rb +9 -0
- data/app/models/spree/null_promotion_handler.rb +44 -0
- data/app/models/spree/order.rb +15 -22
- data/app/models/spree/order_cancellations.rb +8 -8
- data/app/models/spree/order_mutex.rb +2 -2
- data/app/models/spree/order_shipping.rb +9 -9
- data/app/models/spree/order_updater.rb +6 -10
- data/app/models/spree/payment/processing.rb +2 -2
- data/app/models/spree/payment.rb +2 -2
- data/app/models/spree/payment_create.rb +1 -1
- data/app/models/spree/payment_method/store_credit.rb +1 -1
- data/app/models/spree/payment_source.rb +5 -1
- data/app/models/spree/permission_set.rb +11 -0
- data/app/models/spree/product.rb +7 -28
- data/app/models/spree/refund.rb +1 -1
- data/app/models/spree/reimbursement.rb +4 -4
- data/app/models/spree/reimbursement_performer.rb +3 -3
- data/app/models/spree/reimbursement_tax_calculator.rb +2 -2
- data/app/models/spree/reimbursement_type/credit.rb +1 -1
- data/app/models/spree/reimbursement_type/reimbursement_helpers.rb +6 -6
- data/app/models/spree/reimbursement_type/store_credit.rb +1 -1
- data/app/models/spree/return_item.rb +9 -9
- data/app/models/spree/role.rb +3 -1
- data/app/models/spree/role_permission.rb +8 -0
- data/app/models/spree/shipment.rb +7 -7
- data/app/models/spree/shipping_category.rb +2 -0
- data/app/models/spree/shipping_rate.rb +1 -1
- data/app/models/spree/{order_contents.rb → simple_order_contents.rb} +4 -11
- data/app/models/spree/stock/estimator.rb +1 -1
- data/app/models/spree/stock/inventory_unit_builder.rb +1 -1
- data/app/models/spree/stock/package.rb +2 -2
- data/app/models/spree/stock/simple_coordinator.rb +1 -1
- data/app/models/spree/stock_location.rb +5 -5
- data/app/models/spree/store.rb +1 -1
- data/app/models/spree/store_credit.rb +17 -15
- data/app/models/spree/store_credit_event.rb +3 -3
- data/app/models/spree/tax_calculator/default.rb +2 -2
- data/app/models/spree/tax_calculator/shipping_rate.rb +1 -1
- data/app/models/spree/tax_category.rb +3 -1
- data/app/models/spree/tax_rate.rb +3 -3
- data/app/models/spree/taxonomy.rb +2 -2
- data/app/models/spree/unit_cancel.rb +1 -2
- data/app/models/spree/variant/vat_price_generator.rb +1 -1
- data/app/models/spree/variant.rb +7 -2
- data/app/models/spree/wallet.rb +2 -2
- data/app/views/spree/order_mailer/cancel_email.html.erb +1 -1
- data/app/views/spree/order_mailer/cancel_email.text.erb +1 -1
- data/app/views/spree/order_mailer/confirm_email.html.erb +5 -5
- data/app/views/spree/order_mailer/confirm_email.text.erb +5 -5
- data/config/locales/en.yml +4 -90
- data/db/default/spree/permission_sets.rb +10 -0
- data/db/default/spree/return_reasons.rb +3 -1
- data/db/default/spree/states.rb +1 -1
- data/db/migrate/20160101010000_solidus_one_four.rb +0 -117
- data/db/migrate/20240821173254_create_spree_permission_sets_in_core.rb +9 -0
- data/db/migrate/20240821173341_create_spree_roles_permissions_in_core.rb +9 -0
- data/db/migrate/20240821173641_add_description_to_spree_roles.rb +5 -0
- data/db/migrate/20240904152041_add_privilege_and_category_to_spree_permission_sets.rb +6 -0
- data/db/seeds.rb +1 -0
- data/lib/generators/solidus/install/app_templates/frontend/starter.rb +1 -1
- data/lib/generators/solidus/update/update_generator.rb +1 -1
- data/lib/generators/spree/dummy/templates/rails/test.rb +6 -1
- data/lib/spree/app_configuration.rb +53 -84
- data/lib/spree/core/class_constantizer.rb +2 -2
- data/lib/spree/core/controller_helpers/payment_parameters.rb +1 -1
- data/lib/spree/core/engine.rb +1 -0
- data/lib/spree/core/environment/calculators.rb +35 -3
- data/lib/spree/core/environment/promotions.rb +25 -4
- data/lib/spree/core/environment_extension.rb +16 -2
- data/lib/spree/core/importer/order.rb +5 -5
- data/lib/spree/core/importer/product.rb +3 -3
- data/lib/spree/core/nested_class_set.rb +28 -0
- data/lib/spree/core/null_promotion_configuration.rb +84 -0
- data/lib/spree/core/product_filters.rb +1 -1
- data/lib/spree/core/search/variant.rb +2 -2
- data/lib/spree/core/state_machines/order.rb +0 -1
- data/lib/spree/core/validators/email.rb +1 -1
- data/lib/spree/core/version.rb +2 -2
- data/lib/spree/core.rb +2 -6
- data/lib/spree/deprecator.rb +9 -0
- data/lib/spree/i18n.rb +1 -1
- data/lib/spree/mailer_previews/carton_preview.rb +1 -1
- data/lib/spree/permission_sets/base.rb +13 -1
- data/lib/spree/permission_sets/configuration_display.rb +10 -0
- data/lib/spree/permission_sets/configuration_management.rb +10 -0
- data/lib/spree/permission_sets/dashboard_display.rb +12 -0
- data/lib/spree/permission_sets/default_customer.rb +11 -1
- data/lib/spree/permission_sets/order_display.rb +10 -0
- data/lib/spree/permission_sets/order_management.rb +10 -0
- data/lib/spree/permission_sets/product_display.rb +10 -0
- data/lib/spree/permission_sets/product_management.rb +10 -0
- data/lib/spree/permission_sets/restricted_stock_display.rb +10 -0
- data/lib/spree/permission_sets/restricted_stock_management.rb +10 -0
- data/lib/spree/permission_sets/stock_display.rb +10 -0
- data/lib/spree/permission_sets/stock_management.rb +10 -0
- data/lib/spree/permission_sets/super_user.rb +10 -0
- data/lib/spree/permission_sets/user_display.rb +10 -0
- data/lib/spree/permission_sets/user_management.rb +10 -0
- data/lib/spree/permission_sets.rb +0 -2
- data/lib/spree/permitted_attributes.rb +2 -2
- data/lib/spree/preferences/store.rb +2 -2
- data/lib/spree/testing_support/capybara_ext.rb +1 -1
- data/lib/spree/testing_support/dummy_ability.rb +7 -0
- data/lib/spree/testing_support/dummy_app/database.yml +2 -2
- data/lib/spree/testing_support/dummy_app/migrations.rb +8 -15
- data/lib/spree/testing_support/dummy_app/rake_tasks.rb +2 -2
- data/lib/spree/testing_support/dummy_app.rb +17 -12
- data/lib/spree/testing_support/factories/adjustment_factory.rb +1 -2
- data/lib/spree/testing_support/factories/calculator_factory.rb +1 -5
- data/lib/spree/testing_support/factories/carton_factory.rb +2 -2
- data/lib/spree/testing_support/factories/customer_return_factory.rb +2 -2
- data/lib/spree/testing_support/factories/inventory_unit_factory.rb +3 -3
- data/lib/spree/testing_support/factories/order_factory.rb +5 -24
- data/lib/spree/testing_support/factories/product_factory.rb +1 -1
- data/lib/spree/testing_support/factories/shipment_factory.rb +1 -1
- data/lib/spree/testing_support/factories/stock_item_factory.rb +5 -1
- data/lib/spree/testing_support/factories/stock_package_factory.rb +1 -1
- data/lib/spree/testing_support/factories/user_factory.rb +1 -1
- data/lib/spree/testing_support/factories/zone_factory.rb +1 -1
- data/lib/spree/testing_support/order_walkthrough.rb +3 -3
- data/lib/spree/testing_support/shared_examples/calculator.rb +10 -0
- data/lib/spree/testing_support/shared_examples/order_factory.rb +141 -0
- data/lib/spree/testing_support/shared_examples/working_factory.rb +15 -0
- data/lib/tasks/colorado_delivery_fee.rake +2 -2
- data/solidus_core.gemspec +6 -6
- metadata +60 -91
- data/app/jobs/spree/promotion_code_batch_job.rb +0 -26
- data/app/mailers/spree/promotion_code_batch_mailer.rb +0 -15
- data/app/models/spree/calculator/distributed_amount.rb +0 -33
- data/app/models/spree/calculator/flat_percent_item_total.rb +0 -23
- data/app/models/spree/calculator/flexi_rate.rb +0 -22
- data/app/models/spree/calculator/percent_on_line_item.rb +0 -13
- data/app/models/spree/calculator/tiered_flat_rate.rb +0 -52
- data/app/models/spree/calculator/tiered_percent.rb +0 -62
- data/app/models/spree/order_promotion.rb +0 -27
- data/app/models/spree/promotion/actions/create_adjustment.rb +0 -81
- data/app/models/spree/promotion/actions/create_item_adjustments.rb +0 -98
- data/app/models/spree/promotion/actions/create_quantity_adjustments.rb +0 -139
- data/app/models/spree/promotion/actions/free_shipping.rb +0 -59
- data/app/models/spree/promotion/order_adjustments_recalculator.rb +0 -92
- data/app/models/spree/promotion/rules/first_order.rb +0 -38
- data/app/models/spree/promotion/rules/first_repeat_purchase_since.rb +0 -36
- data/app/models/spree/promotion/rules/item_total.rb +0 -86
- data/app/models/spree/promotion/rules/minimum_quantity.rb +0 -59
- data/app/models/spree/promotion/rules/nth_order.rb +0 -45
- data/app/models/spree/promotion/rules/one_use_per_user.rb +0 -25
- data/app/models/spree/promotion/rules/option_value.rb +0 -50
- data/app/models/spree/promotion/rules/product.rb +0 -86
- data/app/models/spree/promotion/rules/store.rb +0 -26
- data/app/models/spree/promotion/rules/taxon.rb +0 -91
- data/app/models/spree/promotion/rules/user.rb +0 -34
- data/app/models/spree/promotion/rules/user_logged_in.rb +0 -20
- data/app/models/spree/promotion/rules/user_role.rb +0 -45
- data/app/models/spree/promotion.rb +0 -271
- data/app/models/spree/promotion_action.rb +0 -47
- data/app/models/spree/promotion_category.rb +0 -8
- data/app/models/spree/promotion_chooser.rb +0 -34
- data/app/models/spree/promotion_code/batch_builder.rb +0 -64
- data/app/models/spree/promotion_code.rb +0 -54
- data/app/models/spree/promotion_code_batch.rb +0 -27
- data/app/models/spree/promotion_handler/cart.rb +0 -75
- data/app/models/spree/promotion_handler/coupon.rb +0 -123
- data/app/models/spree/promotion_handler/page.rb +0 -26
- data/app/models/spree/promotion_handler/shipping.rb +0 -61
- data/app/models/spree/promotion_rule.rb +0 -55
- data/app/models/spree/promotion_rule_store.rb +0 -10
- data/app/models/spree/promotion_rule_taxon.rb +0 -8
- data/app/models/spree/promotion_rule_user.rb +0 -10
- data/app/views/spree/promotion_code_batch_mailer/promotion_code_batch_errored.text.erb +0 -2
- data/app/views/spree/promotion_code_batch_mailer/promotion_code_batch_finished.text.erb +0 -2
- data/db/migrate/20161017102621_create_spree_promotion_code_batch.rb +0 -38
- data/db/migrate/20180202190713_create_promotion_rule_stores.rb +0 -12
- data/db/migrate/20180328172631_add_join_characters_to_promotion_code_batch.rb +0 -11
- data/db/migrate/20190106184413_remove_code_from_spree_promotions.rb +0 -42
- data/db/migrate/20220317165036_set_promotions_with_any_policy_to_all_if_possible.rb +0 -20
- data/db/migrate/20230322085416_remove_match_policy_from_spree_promotion.rb +0 -5
- data/db/migrate/20230325132905_remove_unused_columns_from_promotion_rules.rb +0 -6
- data/db/migrate/20230325161633_drop_unused_promo_action_line_items.rb +0 -13
- data/db/migrate/20231027084517_add_order_promotions_foreign_key.rb +0 -10
- data/db/migrate/20231031175215_add_promotion_order_promotion_foreign_key.rb +0 -10
- data/lib/solidus/migrations/promotions_with_code_handlers.rb +0 -66
- data/lib/spree/permission_sets/promotion_display.rb +0 -25
- data/lib/spree/permission_sets/promotion_management.rb +0 -25
- data/lib/spree/ransack_4_1_patch.rb +0 -16
- data/lib/spree/testing_support/factories/order_promotion_factory.rb +0 -8
- data/lib/spree/testing_support/factories/promotion_category_factory.rb +0 -7
- data/lib/spree/testing_support/factories/promotion_code_factory.rb +0 -8
- 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,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
|