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,52 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_dependency 'spree/calculator'
|
4
|
-
|
5
|
-
module Spree
|
6
|
-
class Calculator::TieredFlatRate < Calculator
|
7
|
-
preference :base_amount, :decimal, default: 0
|
8
|
-
preference :tiers, :hash, default: {}
|
9
|
-
preference :currency, :string, default: -> { Spree::Config[:currency] }
|
10
|
-
|
11
|
-
before_validation do
|
12
|
-
# Convert tier values to decimals. Strings don't do us much good.
|
13
|
-
if preferred_tiers.is_a?(Hash)
|
14
|
-
self.preferred_tiers = preferred_tiers.map do |key, value|
|
15
|
-
[cast_to_d(key.to_s), cast_to_d(value.to_s)]
|
16
|
-
end.to_h
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
validate :preferred_tiers_content
|
21
|
-
|
22
|
-
def compute(object)
|
23
|
-
_base, amount = preferred_tiers.sort.reverse.detect do |value, _|
|
24
|
-
object.amount >= value
|
25
|
-
end
|
26
|
-
|
27
|
-
if preferred_currency.casecmp(object.currency).zero?
|
28
|
-
amount || preferred_base_amount
|
29
|
-
else
|
30
|
-
0
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
private
|
35
|
-
|
36
|
-
def cast_to_d(value)
|
37
|
-
value.to_s.to_d
|
38
|
-
rescue ArgumentError
|
39
|
-
BigDecimal(0)
|
40
|
-
end
|
41
|
-
|
42
|
-
def preferred_tiers_content
|
43
|
-
if preferred_tiers.is_a? Hash
|
44
|
-
unless preferred_tiers.keys.all?{ |key| key.is_a?(Numeric) && key > 0 }
|
45
|
-
errors.add(:base, :keys_should_be_positive_number)
|
46
|
-
end
|
47
|
-
else
|
48
|
-
errors.add(:preferred_tiers, :should_be_hash)
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
@@ -1,62 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_dependency 'spree/calculator'
|
4
|
-
|
5
|
-
module Spree
|
6
|
-
class Calculator::TieredPercent < Calculator
|
7
|
-
preference :base_percent, :decimal, default: 0
|
8
|
-
preference :tiers, :hash, default: {}
|
9
|
-
preference :currency, :string, default: -> { Spree::Config[:currency] }
|
10
|
-
|
11
|
-
before_validation do
|
12
|
-
# Convert tier values to decimals. Strings don't do us much good.
|
13
|
-
if preferred_tiers.is_a?(Hash)
|
14
|
-
self.preferred_tiers = preferred_tiers.map do |key, value|
|
15
|
-
[cast_to_d(key.to_s), cast_to_d(value.to_s)]
|
16
|
-
end.to_h
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
validates :preferred_base_percent, numericality: {
|
21
|
-
greater_than_or_equal_to: 0,
|
22
|
-
less_than_or_equal_to: 100
|
23
|
-
}
|
24
|
-
validate :preferred_tiers_content
|
25
|
-
|
26
|
-
def compute(object)
|
27
|
-
order = object.is_a?(Order) ? object : object.order
|
28
|
-
|
29
|
-
_base, percent = preferred_tiers.sort.reverse.detect do |value, _|
|
30
|
-
order.item_total >= value
|
31
|
-
end
|
32
|
-
|
33
|
-
if preferred_currency.casecmp(order.currency).zero?
|
34
|
-
currency_exponent = ::Money::Currency.find(preferred_currency).exponent
|
35
|
-
(object.amount * (percent || preferred_base_percent) / 100).round(currency_exponent)
|
36
|
-
else
|
37
|
-
0
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
private
|
42
|
-
|
43
|
-
def cast_to_d(value)
|
44
|
-
value.to_s.to_d
|
45
|
-
rescue ArgumentError
|
46
|
-
BigDecimal(0)
|
47
|
-
end
|
48
|
-
|
49
|
-
def preferred_tiers_content
|
50
|
-
if preferred_tiers.is_a? Hash
|
51
|
-
unless preferred_tiers.keys.all?{ |key| key.is_a?(Numeric) && key > 0 }
|
52
|
-
errors.add(:base, :keys_should_be_positive_number)
|
53
|
-
end
|
54
|
-
unless preferred_tiers.values.all?{ |key| key.is_a?(Numeric) && key >= 0 && key <= 100 }
|
55
|
-
errors.add(:base, :values_should_be_percent)
|
56
|
-
end
|
57
|
-
else
|
58
|
-
errors.add(:preferred_tiers, :should_be_hash)
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
@@ -1,27 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Spree
|
4
|
-
# Spree::OrderPromotion represents the relationship between:
|
5
|
-
#
|
6
|
-
# 1. A promotion that a user attempted to apply to their order
|
7
|
-
# 2. The specific code that they used
|
8
|
-
class OrderPromotion < Spree::Base
|
9
|
-
self.table_name = 'spree_orders_promotions'
|
10
|
-
|
11
|
-
belongs_to :order, class_name: 'Spree::Order', optional: true
|
12
|
-
belongs_to :promotion, class_name: 'Spree::Promotion', optional: true
|
13
|
-
belongs_to :promotion_code, class_name: 'Spree::PromotionCode', optional: true
|
14
|
-
|
15
|
-
validates :order, presence: true
|
16
|
-
validates :promotion, presence: true
|
17
|
-
validates :promotion_code, presence: true, if: :require_promotion_code?
|
18
|
-
|
19
|
-
self.allowed_ransackable_associations = %w[promotion_code]
|
20
|
-
|
21
|
-
private
|
22
|
-
|
23
|
-
def require_promotion_code?
|
24
|
-
promotion && !promotion.apply_automatically && promotion.codes.any?
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
@@ -1,81 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Spree
|
4
|
-
class Promotion < Spree::Base
|
5
|
-
module Actions
|
6
|
-
class CreateAdjustment < PromotionAction
|
7
|
-
include Spree::CalculatedAdjustments
|
8
|
-
include Spree::AdjustmentSource
|
9
|
-
|
10
|
-
has_many :adjustments, as: :source
|
11
|
-
|
12
|
-
delegate :eligible?, to: :promotion
|
13
|
-
|
14
|
-
before_validation :ensure_action_has_calculator
|
15
|
-
before_destroy :remove_adjustments_from_incomplete_orders
|
16
|
-
before_discard :remove_adjustments_from_incomplete_orders
|
17
|
-
|
18
|
-
def preload_relations
|
19
|
-
[:calculator]
|
20
|
-
end
|
21
|
-
|
22
|
-
# Creates the adjustment related to a promotion for the order passed
|
23
|
-
# through options hash
|
24
|
-
#
|
25
|
-
# Returns `true` if an adjustment is applied to an order,
|
26
|
-
# `false` if the promotion has already been applied.
|
27
|
-
def perform(options = {})
|
28
|
-
order = options[:order]
|
29
|
-
return if promotion_credit_exists?(order)
|
30
|
-
|
31
|
-
amount = compute_amount(order)
|
32
|
-
order.adjustments.create!(
|
33
|
-
amount: amount,
|
34
|
-
order: order,
|
35
|
-
source: self,
|
36
|
-
promotion_code: options[:promotion_code],
|
37
|
-
label: I18n.t('spree.adjustment_labels.order', promotion: Spree::Promotion.model_name.human, promotion_name: promotion.name)
|
38
|
-
)
|
39
|
-
true
|
40
|
-
end
|
41
|
-
|
42
|
-
# Ensure a negative amount which does not exceed the sum of the order's
|
43
|
-
# item_total and ship_total
|
44
|
-
def compute_amount(calculable)
|
45
|
-
amount = calculator.compute(calculable)
|
46
|
-
amount ||= BigDecimal(0)
|
47
|
-
amount = amount.abs
|
48
|
-
[(calculable.item_total + calculable.ship_total), amount].min * -1
|
49
|
-
end
|
50
|
-
|
51
|
-
# Removes any adjustments generated by this action from the order.
|
52
|
-
# @param order [Spree::Order] the order to remove the action from.
|
53
|
-
# @return [void]
|
54
|
-
def remove_from(order)
|
55
|
-
order.adjustments.each do |adjustment|
|
56
|
-
if adjustment.source == self
|
57
|
-
order.adjustments.destroy(adjustment)
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
private
|
63
|
-
|
64
|
-
# Tells us if there if the specified promotion is already associated with the line item
|
65
|
-
# regardless of whether or not its currently eligible. Useful because generally
|
66
|
-
# you would only want a promotion action to apply to order no more than once.
|
67
|
-
#
|
68
|
-
# Receives an adjustment +source+ (here a PromotionAction object) and tells
|
69
|
-
# if the order has adjustments from that already
|
70
|
-
def promotion_credit_exists?(adjustable)
|
71
|
-
adjustments.where(adjustable_id: adjustable.id).exists?
|
72
|
-
end
|
73
|
-
|
74
|
-
def ensure_action_has_calculator
|
75
|
-
return if calculator
|
76
|
-
self.calculator = Calculator::FlatPercentItemTotal.new
|
77
|
-
end
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
@@ -1,98 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Spree
|
4
|
-
class Promotion < Spree::Base
|
5
|
-
module Actions
|
6
|
-
class CreateItemAdjustments < PromotionAction
|
7
|
-
include Spree::CalculatedAdjustments
|
8
|
-
include Spree::AdjustmentSource
|
9
|
-
|
10
|
-
has_many :adjustments, as: :source
|
11
|
-
|
12
|
-
delegate :eligible?, to: :promotion
|
13
|
-
|
14
|
-
before_validation :ensure_action_has_calculator
|
15
|
-
before_destroy :remove_adjustments_from_incomplete_orders
|
16
|
-
before_discard :remove_adjustments_from_incomplete_orders
|
17
|
-
|
18
|
-
def preload_relations
|
19
|
-
[:calculator]
|
20
|
-
end
|
21
|
-
|
22
|
-
def perform(payload = {})
|
23
|
-
order = payload[:order]
|
24
|
-
promotion = payload[:promotion]
|
25
|
-
promotion_code = payload[:promotion_code]
|
26
|
-
|
27
|
-
results = line_items_to_adjust(promotion, order).map do |line_item|
|
28
|
-
create_adjustment(line_item, order, promotion_code)
|
29
|
-
end
|
30
|
-
|
31
|
-
results.any?
|
32
|
-
end
|
33
|
-
|
34
|
-
# Ensure a negative amount which does not exceed the sum of the order's
|
35
|
-
# item_total and ship_total
|
36
|
-
def compute_amount(adjustable)
|
37
|
-
order = adjustable.is_a?(Order) ? adjustable : adjustable.order
|
38
|
-
return 0 unless promotion.line_item_actionable?(order, adjustable)
|
39
|
-
promotion_amount = calculator.compute(adjustable)
|
40
|
-
promotion_amount ||= BigDecimal(0)
|
41
|
-
promotion_amount = promotion_amount.abs
|
42
|
-
[adjustable.amount, promotion_amount].min * -1
|
43
|
-
end
|
44
|
-
|
45
|
-
# Removes any adjustments generated by this action from the order's
|
46
|
-
# line items.
|
47
|
-
# @param order [Spree::Order] the order to remove the action from.
|
48
|
-
# @return [void]
|
49
|
-
def remove_from(order)
|
50
|
-
order.line_items.each do |line_item|
|
51
|
-
line_item.adjustments.each do |adjustment|
|
52
|
-
if adjustment.source == self
|
53
|
-
line_item.adjustments.destroy(adjustment)
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
private
|
60
|
-
|
61
|
-
def create_adjustment(adjustable, order, promotion_code)
|
62
|
-
amount = compute_amount(adjustable)
|
63
|
-
return if amount == 0
|
64
|
-
adjustable.adjustments.create!(
|
65
|
-
source: self,
|
66
|
-
amount: amount,
|
67
|
-
order: order,
|
68
|
-
promotion_code: promotion_code,
|
69
|
-
label: I18n.t('spree.adjustment_labels.line_item', promotion: Spree::Promotion.model_name.human, promotion_name: promotion.name)
|
70
|
-
)
|
71
|
-
true
|
72
|
-
end
|
73
|
-
|
74
|
-
# Tells us if there if the specified promotion is already associated with the line item
|
75
|
-
# regardless of whether or not its currently eligible. Useful because generally
|
76
|
-
# you would only want a promotion action to apply to line item no more than once.
|
77
|
-
#
|
78
|
-
# Receives an adjustment +source+ (here a PromotionAction object) and tells
|
79
|
-
# if the order has adjustments from that already
|
80
|
-
def promotion_credit_exists?(adjustable)
|
81
|
-
adjustments.where(adjustable_id: adjustable.id).exists?
|
82
|
-
end
|
83
|
-
|
84
|
-
def ensure_action_has_calculator
|
85
|
-
return if calculator
|
86
|
-
self.calculator = Calculator::PercentOnLineItem.new
|
87
|
-
end
|
88
|
-
|
89
|
-
def line_items_to_adjust(promotion, order)
|
90
|
-
order.line_items.select do |line_item|
|
91
|
-
line_item.adjustments.none? { |adjustment| adjustment.source == self } &&
|
92
|
-
promotion.line_item_actionable?(order, line_item)
|
93
|
-
end
|
94
|
-
end
|
95
|
-
end
|
96
|
-
end
|
97
|
-
end
|
98
|
-
end
|
@@ -1,139 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Spree
|
4
|
-
class Promotion < Spree::Base
|
5
|
-
module Actions
|
6
|
-
class CreateQuantityAdjustments < CreateItemAdjustments
|
7
|
-
preference :group_size, :integer, default: 1
|
8
|
-
|
9
|
-
has_many :line_item_actions, foreign_key: :action_id, dependent: :destroy
|
10
|
-
has_many :line_items, through: :line_item_actions
|
11
|
-
|
12
|
-
##
|
13
|
-
# Computes the amount for the adjustment based on the line item and any
|
14
|
-
# other applicable items in the order. The rules for this specific
|
15
|
-
# adjustment are as follows:
|
16
|
-
#
|
17
|
-
# = Setup
|
18
|
-
#
|
19
|
-
# We have a quantity group promotion on t-shirts. If a user orders 3
|
20
|
-
# t-shirts, they get $5 off of each. The shirts come in one size and three
|
21
|
-
# colours: red, blue, and white.
|
22
|
-
#
|
23
|
-
# == Scenario 1
|
24
|
-
#
|
25
|
-
# User has 2 red shirts, 1 white shirt, and 1 blue shirt in their
|
26
|
-
# order. We want to compute the adjustment amount for the white shirt.
|
27
|
-
#
|
28
|
-
# *Result:* -$5
|
29
|
-
#
|
30
|
-
# *Reasoning:* There are a total of 4 items that are eligible for the
|
31
|
-
# promotion. Since that is greater than 3, we can discount the items. The
|
32
|
-
# white shirt has a quantity of 1, therefore it will get discounted by
|
33
|
-
# +adjustment_amount * 1+ or $5.
|
34
|
-
#
|
35
|
-
# === Scenario 1-1
|
36
|
-
#
|
37
|
-
# What about the blue shirt? How much does it get discounted?
|
38
|
-
#
|
39
|
-
# *Result:* $0
|
40
|
-
#
|
41
|
-
# *Reasoning:* We have a total quantity of 4. However, we only apply the
|
42
|
-
# adjustment to groups of 3. Assuming the white and red shirts have already
|
43
|
-
# had their adjustment calculated, that means 3 units have been discounted.
|
44
|
-
# Leaving us with a lonely blue shirt that isn't part of a group of 3.
|
45
|
-
# Therefore, it does not receive the discount.
|
46
|
-
#
|
47
|
-
# == Scenario 2
|
48
|
-
#
|
49
|
-
# User has 4 red shirts in their order. What is the amount?
|
50
|
-
#
|
51
|
-
# *Result:* -$15
|
52
|
-
#
|
53
|
-
# *Reasoning:* The total quantity of eligible items is 4, so we the
|
54
|
-
# adjustment will be non-zero. However, we only apply it to groups of 3,
|
55
|
-
# therefore there is one extra item that is not eligible for the
|
56
|
-
# adjustment. +adjustment_amount * 3+ or $15.
|
57
|
-
#
|
58
|
-
def compute_amount(line_item)
|
59
|
-
adjustment_amount = calculator.compute(PartialLineItem.new(line_item))
|
60
|
-
adjustment_amount ||= BigDecimal(0)
|
61
|
-
adjustment_amount = adjustment_amount.abs
|
62
|
-
|
63
|
-
order = line_item.order
|
64
|
-
line_items = actionable_line_items(order)
|
65
|
-
|
66
|
-
actioned_line_items = order.line_item_adjustments.reload.
|
67
|
-
select { |adjustment| adjustment.source == self && adjustment.amount < 0 }.
|
68
|
-
map(&:adjustable)
|
69
|
-
other_line_items = actioned_line_items - [line_item]
|
70
|
-
|
71
|
-
applicable_quantity = total_applicable_quantity(line_items)
|
72
|
-
used_quantity = total_used_quantity(other_line_items)
|
73
|
-
usable_quantity = [
|
74
|
-
applicable_quantity - used_quantity,
|
75
|
-
line_item.quantity
|
76
|
-
].min
|
77
|
-
|
78
|
-
persist_quantity(usable_quantity, line_item)
|
79
|
-
|
80
|
-
amount = adjustment_amount * usable_quantity
|
81
|
-
[line_item.amount, amount].min * -1
|
82
|
-
end
|
83
|
-
|
84
|
-
private
|
85
|
-
|
86
|
-
def actionable_line_items(order)
|
87
|
-
order.line_items.select do |item|
|
88
|
-
promotion.line_item_actionable? order, item
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
def total_applicable_quantity(line_items)
|
93
|
-
total_quantity = line_items.sum(&:quantity)
|
94
|
-
extra_quantity = total_quantity % preferred_group_size
|
95
|
-
|
96
|
-
total_quantity - extra_quantity
|
97
|
-
end
|
98
|
-
|
99
|
-
def total_used_quantity(line_items)
|
100
|
-
line_item_actions.where(
|
101
|
-
line_item_id: line_items.map(&:id)
|
102
|
-
).sum(:quantity)
|
103
|
-
end
|
104
|
-
|
105
|
-
def persist_quantity(quantity, line_item)
|
106
|
-
line_item_action = line_item_actions.where(
|
107
|
-
line_item_id: line_item.id
|
108
|
-
).first_or_initialize
|
109
|
-
line_item_action.quantity = quantity
|
110
|
-
line_item_action.save!
|
111
|
-
end
|
112
|
-
|
113
|
-
##
|
114
|
-
# Used specifically for PercentOnLineItem calculator. That calculator uses
|
115
|
-
# `line_item.amount`, however we might not necessarily want to discount the
|
116
|
-
# entire amount. This class allows us to determine the discount per
|
117
|
-
# quantity and then calculate the adjustment amount the way we normally do
|
118
|
-
# for flat rate adjustments.
|
119
|
-
class PartialLineItem
|
120
|
-
def initialize(line_item)
|
121
|
-
@line_item = line_item
|
122
|
-
end
|
123
|
-
|
124
|
-
def amount
|
125
|
-
@line_item.price
|
126
|
-
end
|
127
|
-
|
128
|
-
def order
|
129
|
-
@line_item.order
|
130
|
-
end
|
131
|
-
|
132
|
-
def currency
|
133
|
-
@line_item.currency
|
134
|
-
end
|
135
|
-
end
|
136
|
-
end
|
137
|
-
end
|
138
|
-
end
|
139
|
-
end
|
@@ -1,59 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Spree
|
4
|
-
class Promotion < Spree::Base
|
5
|
-
module Actions
|
6
|
-
class FreeShipping < Spree::PromotionAction
|
7
|
-
def perform(payload = {})
|
8
|
-
order = payload[:order]
|
9
|
-
promotion_code = payload[:promotion_code]
|
10
|
-
return false unless promotion.eligible? order
|
11
|
-
|
12
|
-
created_adjustments = order.shipments.map do |shipment|
|
13
|
-
next if promotion_credit_exists?(shipment)
|
14
|
-
|
15
|
-
shipment.adjustments.create!(
|
16
|
-
order: shipment.order,
|
17
|
-
amount: compute_amount(shipment),
|
18
|
-
source: self,
|
19
|
-
promotion_code: promotion_code,
|
20
|
-
label: label
|
21
|
-
)
|
22
|
-
end
|
23
|
-
|
24
|
-
# Did we actually end up creating any adjustments?
|
25
|
-
# If so, then this action should be classed as 'successful'
|
26
|
-
created_adjustments.any?
|
27
|
-
end
|
28
|
-
|
29
|
-
def label
|
30
|
-
"#{I18n.t('spree.promotion')} (#{promotion.name})"
|
31
|
-
end
|
32
|
-
|
33
|
-
def compute_amount(shipment)
|
34
|
-
shipment.cost * -1
|
35
|
-
end
|
36
|
-
|
37
|
-
# Removes any adjustments generated by this action from the order's
|
38
|
-
# shipments.
|
39
|
-
# @param order [Spree::Order] the order to remove the action from.
|
40
|
-
# @return [void]
|
41
|
-
def remove_from(order)
|
42
|
-
order.shipments.each do |shipment|
|
43
|
-
shipment.adjustments.each do |adjustment|
|
44
|
-
if adjustment.source == self
|
45
|
-
shipment.adjustments.destroy(adjustment)
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
private
|
52
|
-
|
53
|
-
def promotion_credit_exists?(shipment)
|
54
|
-
shipment.adjustments.where(source: self).exists?
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|
59
|
-
end
|
@@ -1,92 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Spree
|
4
|
-
class Promotion < Spree::Base
|
5
|
-
|
6
|
-
# This class encapsulates all the things the promotion system does to
|
7
|
-
# an order. It is called from the `Spree::OrderUpdater` before taxes are
|
8
|
-
# calculated, such that taxes always respect promotions.
|
9
|
-
|
10
|
-
# This class iterates over all existing promotion adjustments and recalculates
|
11
|
-
# their amount and eligibility using their adjustment source.
|
12
|
-
class OrderAdjustmentsRecalculator
|
13
|
-
def initialize(order)
|
14
|
-
@order = order
|
15
|
-
end
|
16
|
-
|
17
|
-
def call
|
18
|
-
all_items = line_items + shipments
|
19
|
-
all_items.each do |item|
|
20
|
-
promotion_adjustments = item.adjustments.select(&:promotion?)
|
21
|
-
|
22
|
-
promotion_adjustments.each { |adjustment| recalculate(adjustment) }
|
23
|
-
Spree::Config.promotion_chooser_class.new(promotion_adjustments).update
|
24
|
-
|
25
|
-
item.promo_total = promotion_adjustments.select(&:eligible?).sum(&:amount)
|
26
|
-
end
|
27
|
-
# Update and select the best promotion adjustment for the order.
|
28
|
-
# We don't update the order.promo_total yet. Order totals are updated later
|
29
|
-
# in #update_adjustment_total since they include the totals from the order's
|
30
|
-
# line items and/or shipments.
|
31
|
-
order_promotion_adjustments = order.adjustments.select(&:promotion?)
|
32
|
-
order_promotion_adjustments.each { |adjustment| recalculate(adjustment) }
|
33
|
-
Spree::Config.promotion_chooser_class.new(order_promotion_adjustments).update
|
34
|
-
|
35
|
-
order.promo_total = all_items.sum(&:promo_total) +
|
36
|
-
order_promotion_adjustments.
|
37
|
-
select(&:eligible?).
|
38
|
-
select(&:promotion?).
|
39
|
-
sum(&:amount)
|
40
|
-
order
|
41
|
-
end
|
42
|
-
|
43
|
-
private
|
44
|
-
|
45
|
-
attr_reader :order
|
46
|
-
delegate :line_items, :shipments, to: :order
|
47
|
-
|
48
|
-
# Recalculate and persist the amount from this adjustment's source based on
|
49
|
-
# the adjustable ({Order}, {Shipment}, or {LineItem})
|
50
|
-
#
|
51
|
-
# If the adjustment has no source (such as when created manually from the
|
52
|
-
# admin) or is closed, this is a noop.
|
53
|
-
#
|
54
|
-
# @return [BigDecimal] New amount of this adjustment
|
55
|
-
def recalculate(adjustment)
|
56
|
-
if adjustment.finalized?
|
57
|
-
return adjustment.amount
|
58
|
-
end
|
59
|
-
|
60
|
-
# If the adjustment has no source, do not attempt to re-calculate the
|
61
|
-
# amount.
|
62
|
-
# Some scenarios where this happens:
|
63
|
-
# - Adjustments that are manually created via the admin backend
|
64
|
-
# - PromotionAction adjustments where the PromotionAction was deleted
|
65
|
-
# after the order was completed.
|
66
|
-
if adjustment.source.present?
|
67
|
-
adjustment.amount = adjustment.source.compute_amount(adjustment.adjustable)
|
68
|
-
|
69
|
-
adjustment.eligible = calculate_eligibility(adjustment)
|
70
|
-
|
71
|
-
# Persist only if changed
|
72
|
-
# This is only not a save! to avoid the extra queries to load the order
|
73
|
-
# (for validations) and to touch the adjustment.
|
74
|
-
adjustment.update_columns(eligible: adjustment.eligible, amount: adjustment.amount, updated_at: Time.current) if adjustment.changed?
|
75
|
-
end
|
76
|
-
adjustment.amount
|
77
|
-
end
|
78
|
-
|
79
|
-
# Calculates based on attached promotion (if this is a promotion
|
80
|
-
# adjustment) whether this promotion is still eligible.
|
81
|
-
# @api private
|
82
|
-
# @return [true,false] Whether this adjustment is eligible
|
83
|
-
def calculate_eligibility(adjustment)
|
84
|
-
if !adjustment.finalized? && adjustment.source
|
85
|
-
adjustment.source.promotion.eligible?(adjustment.adjustable, promotion_code: adjustment.promotion_code)
|
86
|
-
else
|
87
|
-
adjustment.eligible?
|
88
|
-
end
|
89
|
-
end
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|
@@ -1,38 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Spree
|
4
|
-
class Promotion < Spree::Base
|
5
|
-
module Rules
|
6
|
-
class FirstOrder < PromotionRule
|
7
|
-
attr_reader :user, :email
|
8
|
-
|
9
|
-
def applicable?(promotable)
|
10
|
-
promotable.is_a?(Spree::Order)
|
11
|
-
end
|
12
|
-
|
13
|
-
def eligible?(order, options = {})
|
14
|
-
@user = order.try(:user) || options[:user]
|
15
|
-
@email = order.email
|
16
|
-
|
17
|
-
if user || email
|
18
|
-
if !completed_orders.blank? && completed_orders.first != order
|
19
|
-
eligibility_errors.add(:base, eligibility_error_message(:not_first_order), error_code: :not_first_order)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
eligibility_errors.empty?
|
24
|
-
end
|
25
|
-
|
26
|
-
private
|
27
|
-
|
28
|
-
def completed_orders
|
29
|
-
user ? user.orders.complete : orders_by_email
|
30
|
-
end
|
31
|
-
|
32
|
-
def orders_by_email
|
33
|
-
Spree::Order.where(email: email).complete
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
@@ -1,36 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Spree
|
4
|
-
class Promotion < Spree::Base
|
5
|
-
module Rules
|
6
|
-
class FirstRepeatPurchaseSince < PromotionRule
|
7
|
-
preference :days_ago, :integer, default: 365
|
8
|
-
validates :preferred_days_ago, numericality: { only_integer: true, greater_than: 0 }
|
9
|
-
|
10
|
-
# This promotion is applicable to orders only.
|
11
|
-
def applicable?(promotable)
|
12
|
-
promotable.is_a?(Spree::Order)
|
13
|
-
end
|
14
|
-
|
15
|
-
# This is never eligible if the order does not have a user, and that user does not have any previous completed orders.
|
16
|
-
#
|
17
|
-
# This is eligible if the user's most recently completed order is more than the preferred days ago
|
18
|
-
# @param order [Spree::Order]
|
19
|
-
def eligible?(order, _options = {})
|
20
|
-
return false unless order.user
|
21
|
-
|
22
|
-
last_order = last_completed_order(order.user)
|
23
|
-
return false unless last_order
|
24
|
-
|
25
|
-
last_order.completed_at < preferred_days_ago.days.ago
|
26
|
-
end
|
27
|
-
|
28
|
-
private
|
29
|
-
|
30
|
-
def last_completed_order(user)
|
31
|
-
user.orders.complete.order(:completed_at).last
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|