solidus_core 4.3.5 → 4.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 -4
- 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 +15 -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 -127
- 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 +7 -7
- metadata +60 -93
- 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/line_item_action.rb +0 -8
- 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/bin/rails +0 -13
- 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
|