diff --git a/promotions/app/models/solidus_promotions/order_adjuster.rb b/promotions/app/models/solidus_promotions/order_adjuster.rb index 8769670be57..ea16b895166 100644 --- a/promotions/app/models/solidus_promotions/order_adjuster.rb +++ b/promotions/app/models/solidus_promotions/order_adjuster.rb @@ -13,7 +13,7 @@ def initialize(order, dry_run_promotion: nil) def call order.reset_current_discounts - return order if (!SolidusPromotions.config.recalculate_complete_orders && order.complete?) || order.shipped? + return order unless SolidusPromotions::Promotion.order_activatable?(order) discounted_order = DiscountOrder.new(order, promotions, dry_run: dry_run).call diff --git a/promotions/app/models/solidus_promotions/promotion.rb b/promotions/app/models/solidus_promotions/promotion.rb index 7eacbbf61af..fced9b24d75 100644 --- a/promotions/app/models/solidus_promotions/promotion.rb +++ b/promotions/app/models/solidus_promotions/promotion.rb @@ -2,6 +2,8 @@ module SolidusPromotions class Promotion < Spree::Base + UNACTIVATABLE_ORDER_STATES = ["awaiting_return", "returned", "canceled"] + include Spree::SoftDeletable belongs_to :category, class_name: "SolidusPromotions::PromotionCategory", @@ -59,6 +61,14 @@ def self.ordered_lanes lanes.sort_by(&:last).to_h end + def self.order_activatable?(order) + return false if UNACTIVATABLE_ORDER_STATES.include?(order.state) + return false if order.shipped? + return false if order.complete? && !SolidusPromotions.config.recalculate_complete_orders + + true + end + self.allowed_ransackable_associations = ["codes"] self.allowed_ransackable_attributes = %w[name customer_label path promotion_category_id lane updated_at] self.allowed_ransackable_scopes = %i[active with_discarded] diff --git a/promotions/app/models/solidus_promotions/promotion_handler/coupon.rb b/promotions/app/models/solidus_promotions/promotion_handler/coupon.rb index 48db345a41e..99fe4918b66 100644 --- a/promotions/app/models/solidus_promotions/promotion_handler/coupon.rb +++ b/promotions/app/models/solidus_promotions/promotion_handler/coupon.rb @@ -26,6 +26,10 @@ def apply self end + def can_apply? + SolidusPromotions::Promotion.order_activatable?(order) + end + def remove if promotion.blank? set_error_code :coupon_code_not_found diff --git a/promotions/spec/models/solidus_promotions/promotion_handler/coupon_spec.rb b/promotions/spec/models/solidus_promotions/promotion_handler/coupon_spec.rb index 0e82d6c8bdc..7b6df6178de 100644 --- a/promotions/spec/models/solidus_promotions/promotion_handler/coupon_spec.rb +++ b/promotions/spec/models/solidus_promotions/promotion_handler/coupon_spec.rb @@ -452,4 +452,15 @@ def expect_adjustment_creation(adjustable:, promotion:) ) end end + + describe "#can_apply?" do + let(:order) { double("Order").as_null_object } + + subject { described_class.new(order).can_apply? } + + it "forwards to SolidusPromotions::Promotion.order_activatable?" do + expect(SolidusPromotions::Promotion).to receive(:order_activatable?).with(order) + subject + end + end end diff --git a/promotions/spec/models/solidus_promotions/promotion_spec.rb b/promotions/spec/models/solidus_promotions/promotion_spec.rb index e2fea9fad2a..6d941eabf6b 100644 --- a/promotions/spec/models/solidus_promotions/promotion_spec.rb +++ b/promotions/spec/models/solidus_promotions/promotion_spec.rb @@ -686,4 +686,56 @@ expect(subject).to be_nil end end + + describe ".order_activatable" do + let(:order) { create :order } + + subject { described_class.order_activatable?(order) } + + it "is true" do + expect(subject).to be true + end + + context "when the order is in the cart state" do + let(:order) { create :order, state: "cart" } + + it { is_expected.to be true } + end + + context "when the order is shipped" do + let(:order) { create :order, state: "complete", shipment_state: "shipped" } + + it { is_expected.to be false } + end + + context "when the order is completed but not shipped" do + let(:order) { create :order, state: "complete", shipment_state: "ready" } + + it { is_expected.to be true } + + context "when the promotion system is configured to prohibit applying promotions to completed orders" do + before { stub_spree_preferences(SolidusPromotions.configuration, recalculate_complete_orders: false) } + + it { is_expected.to be false } + end + end + + context "when the order is canceled" do + let(:order) { create :order, state: "canceled" } + + it { is_expected.to be false } + end + + context "when the order is awaiting return" do + let(:order) { create :order, state: "awaiting_return" } + + it { is_expected.to be false } + end + + context "when the order is returned" do + let(:order) { create :order, state: "returned" } + + it { is_expected.to be false } + end + end end diff --git a/promotions/spec/system/solidus_promotions/backend/orders/adjustments_spec.rb b/promotions/spec/system/solidus_promotions/backend/orders/adjustments_spec.rb new file mode 100644 index 00000000000..d861d498fa2 --- /dev/null +++ b/promotions/spec/system/solidus_promotions/backend/orders/adjustments_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "Adjustments", type: :feature do + stub_authorization! + + let!(:ship_address) { create(:address) } + let!(:tax_zone) { create(:global_zone) } # will include the above address + let!(:tax_rate) { create(:tax_rate, name: "Sales Tax", amount: 0.20, zone: tax_zone, tax_categories: [tax_category]) } + + let!(:line_item) { order.line_items[0] } + + let(:tax_category) { create(:tax_category) } + let(:variant) { create(:variant, tax_category:) } + let(:preferences) { {} } + + before(:each) do + stub_spree_preferences(SolidusPromotions.configuration, preferences) + order.recalculate + + visit spree.admin_path + click_link "Orders" + uncheck "Only show complete orders" + click_button "Filter Results" + within_row(1) { click_icon :edit } + click_link "Adjustments" + end + + let!(:order) { create(:order, line_items_attributes: [{ price: 10, variant: }]) } + + context "when the order is completed" do + let!(:order) do + create( + :completed_order_with_totals, + line_items_attributes: [{ price: 10, variant: }], + ship_address: + ) + end + + let!(:adjustment) { order.adjustments.create!(order:, label: "Rebate", amount: 10) } + + it "shows adjustments" do + expect(page).to have_content("Adjustments") + end + + context "when the promotion system is configured to allow applying promotions to completed orders" do + it "shows input field for promotion code" do + expect(page).to have_content("Adjustments") + expect(page).to have_field("coupon_code") + end + end + + context "when the promotion system is configured to not allow applying promotions to completed orders" do + let(:preferences) { { recalculate_complete_orders: false } } + + it "does not show input field for promotion code" do + expect(page).to have_content("Adjustments") + expect(page).not_to have_field("coupon_code") + end + end + end + + it "shows the input field for applying a promotion" do + expect(page).to have_field("coupon_code") + end + + context "creating a manual adjustment" do + let!(:adjustment_reason) { create(:adjustment_reason, name: "Friendly customer") } + before do + click_link "New Adjustment" + end + + it "creates a new adjustment" do + fill_in "adjustment_amount", with: "5" + fill_in "adjustment_label", with: "Test Adjustment" + select "Friendly customer", from: "Reason" + click_button "Continue" + expect(page).to have_content("Adjustment has been successfully created!") + expect(page).to have_content("Test Adjustment") + end + end +end