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