Files
openfoodnetwork/spec/jobs/subscription_placement_job_spec.rb
2025-09-05 14:30:46 +10:00

228 lines
8.4 KiB
Ruby

# frozen_string_literal: true
require 'spec_helper'
RSpec.describe SubscriptionPlacementJob do
let(:job) { SubscriptionPlacementJob.new }
let(:summarizer) { OrderManagement::Subscriptions::Summarizer.new }
describe "finding proxy_orders that are ready to be placed" do
let(:shop) { create(:distributor_enterprise) }
let(:order_cycle1) {
create(:simple_order_cycle, coordinator: shop, orders_open_at: 1.minute.ago,
orders_close_at: 10.minutes.from_now)
}
let(:order_cycle2) {
create(:simple_order_cycle, coordinator: shop, orders_open_at: 10.minutes.ago,
orders_close_at: 1.minute.ago)
}
let(:schedule) { create(:schedule, order_cycles: [order_cycle1, order_cycle2]) }
let(:subscription) { create(:subscription, shop:, schedule:) }
let!(:proxy_order) {
create(:proxy_order, subscription:, order_cycle: order_cycle1)
} # OK
it "ignores proxy orders where the OC has closed" do
expect(job.__send__(:proxy_orders)).to include proxy_order
proxy_order.update!(order_cycle_id: order_cycle2.id)
expect(job.__send__(:proxy_orders)).not_to include proxy_order
end
it "ignores proxy orders for paused or cancelled subscriptions" do
expect(job.__send__(:proxy_orders)).to include proxy_order
subscription.update!(paused_at: 1.minute.ago)
expect(job.__send__(:proxy_orders)).not_to include proxy_order
subscription.update!(paused_at: nil)
expect(job.__send__(:proxy_orders)).to include proxy_order
subscription.update!(canceled_at: 1.minute.ago)
expect(job.__send__(:proxy_orders)).not_to include proxy_order
end
it "ignores proxy orders that have been marked as cancelled or placed" do
expect(job.__send__(:proxy_orders)).to include proxy_order
proxy_order.update!(canceled_at: 5.minutes.ago)
expect(job.__send__(:proxy_orders)).not_to include proxy_order
proxy_order.update!(canceled_at: nil)
expect(job.__send__(:proxy_orders)).to include proxy_order
proxy_order.update!(placed_at: 5.minutes.ago)
expect(job.__send__(:proxy_orders)).not_to include proxy_order
end
end
describe "performing the job" do
context "when unplaced proxy_orders exist" do
let!(:subscription) { create(:subscription, with_items: true) }
let(:order) { build(:order, distributor: shop, line_items: [build(:line_item)]) }
let(:shop) { order_cycle.coordinator }
let(:order_cycle) { create(:simple_order_cycle) }
let(:exchange) {
create(:exchange, order_cycle:, sender: shop, receiver: shop, incoming: false)
}
let!(:proxy_order) {
create(:proxy_order, subscription:, order_cycle:, order:)
}
before do
allow(job).to receive(:proxy_orders) { ProxyOrder.where(id: proxy_order.id) }
end
it "processes placeable proxy_orders" do
service = PlaceProxyOrder.new(proxy_order, summarizer, JobLogger.logger, CapQuantity.new)
allow(PlaceProxyOrder).to receive(:new) { service }
allow(service).to receive(:call)
job.perform
expect(service).to have_received(:call)
end
it "records exceptions" do
exchange.variants << order.line_items.first.variant
summarizer = TestSummarizer.new
allow(OrderManagement::Subscriptions::Summarizer).to receive(:new).and_return(summarizer)
job.perform
expect(summarizer.recorded_issues[order.id])
.to eq("Errors: Cannot transition state via :next from :address " \
"(Reason(s): Items cannot be shipped)")
end
end
end
describe "processing a subscription order" do
let!(:shipping_method_created_earlier) { create(:shipping_method, distributors: [shop]) }
let!(:shipping_method) { create(:shipping_method, distributors: [shop]) }
let!(:shipping_method_created_later) { create(:shipping_method, distributors: [shop]) }
let(:shop) { create(:enterprise) }
let(:subscription) { create(:subscription, shop:, with_items: true) }
let(:proxy_order) { create(:proxy_order, subscription:) }
let(:oc) { proxy_order.order_cycle }
let(:ex) { oc.exchanges.outgoing.find_by(sender_id: shop.id, receiver_id: shop.id) }
let(:fee) { create(:enterprise_fee, enterprise: shop, fee_type: 'sales', amount: 10) }
let!(:exchange_fee) { ExchangeFee.create!(exchange: ex, enterprise_fee: fee) }
before do
expect_any_instance_of(Spree::Payment).not_to receive(:process!)
allow_any_instance_of(PlaceProxyOrder).to receive(:send_placement_email)
allow_any_instance_of(PlaceProxyOrder).to receive(:send_empty_email)
end
context "when the order is not already complete" do
context "when no stock items are available after capping stock" do
let(:service) do
PlaceProxyOrder.new(proxy_order, summarizer, JobLogger.logger, store_updater)
end
let(:store_updater) { CapQuantity.new }
before do
fake_relation = instance_double(ActiveRecord::Relation, select: -123)
allow(store_updater).to receive(:available_variants_for).and_return(fake_relation)
end
it "does not place the order, clears all adjustments, and sends an empty_order email" do
allow(service).to receive(:send_placement_email)
allow(service).to receive(:send_empty_email)
service.call
expect(proxy_order.order.reload.completed_at).to be_nil
expect(proxy_order.order.all_adjustments).to be_empty
expect(proxy_order.order.total).to eq 0
expect(proxy_order.order.adjustment_total).to eq 0
expect(service).not_to have_received(:send_placement_email)
expect(service).to have_received(:send_empty_email)
end
end
context "when at least one stock item is available after capping stock" do
let(:service) do
PlaceProxyOrder.new(proxy_order, summarizer, JobLogger.logger, CapQuantity.new)
end
before do
allow(service).to receive(:send_placement_email)
end
it "processes the order to completion, but does not process the payment" do
freeze_time do
service.call
proxy_order.order.reload.completed_at
expect(proxy_order.order.completed_at).to eq(Time.zone.now)
expect(proxy_order.order.payments.first.state).to eq "checkout"
end
end
it "does not enqueue confirmation emails" do
expect{ service.call }
.not_to have_enqueued_mail(Spree::OrderMailer, :confirm_email_for_customer)
expect(service).to have_received(:send_placement_email).once
end
context "when progression of the order fails" do
before { allow(service).to receive(:move_to_completion).and_raise(StandardError) }
it "records an error and does not attempt to send an email" do
expect(service).not_to receive(:send_placement_email)
expect(summarizer).to receive(:record_and_log_error).once
service.call
end
end
end
end
end
describe "parallisation", concurrency: true do
let(:shop) { create(:distributor_enterprise) }
let(:order_cycle) {
create(
:simple_order_cycle,
coordinator: shop,
orders_open_at: 1.minute.ago,
orders_close_at: 10.minutes.from_now
)
}
let(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
let(:subscription) { create(:subscription, shop:, schedule:) }
let!(:proxy_order) {
create(:proxy_order, subscription:, order_cycle:)
}
let(:breakpoint) { Mutex.new }
it "doesn't place duplicate orders" do
# Pause jobs when placing proxy order:
breakpoint.lock
allow(PlaceProxyOrder).to(
receive(:new).and_wrap_original do |method, *args|
breakpoint.synchronize { nil }
method.call(*args)
end
)
expect {
# Start two jobs in parallel:
threads = [
Thread.new { SubscriptionPlacementJob.new.perform },
Thread.new { SubscriptionPlacementJob.new.perform },
]
# Wait for both to jobs to pause.
# This can reveal a race condition.
sleep 1
# Resume and complete both jobs:
breakpoint.unlock
threads.each(&:join)
}.to change {
Spree::Order.count
}.by(1)
end
end
end