diff --git a/app/jobs/schedule_daily_batch_deliveries_job.rb b/app/jobs/schedule_daily_batch_deliveries_job.rb index 545d9112b..8262ee796 100644 --- a/app/jobs/schedule_daily_batch_deliveries_job.rb +++ b/app/jobs/schedule_daily_batch_deliveries_job.rb @@ -10,7 +10,7 @@ def perform date = Time.zone.yesterday batch_begin_at = date.in_time_zone(TimeZoneUtils.submission_time_zone).beginning_of_day - DailySubmissionBatchSelector.batches(date).each do |batch| + BatchSubmissionsSelector.daily_batches(date).each do |batch| existing_deliveries = batch.submissions.first.deliveries.daily if existing_deliveries.any? Rails.logger.warn("Daily batch delivery already exists for batch - skipping", { diff --git a/app/jobs/schedule_weekly_batch_deliveries_job.rb b/app/jobs/schedule_weekly_batch_deliveries_job.rb new file mode 100644 index 000000000..f8f6c308f --- /dev/null +++ b/app/jobs/schedule_weekly_batch_deliveries_job.rb @@ -0,0 +1,38 @@ +class ScheduleWeeklyBatchDeliveriesJob < ApplicationJob + # If we change the queue for this job, ensure we add a new alert in CloudWatch for failed executions + queue_as :submissions + + def perform + CloudWatchService.record_job_started_metric(self.class.name) + CurrentJobLoggingAttributes.job_class = self.class.name + CurrentJobLoggingAttributes.job_id = job_id + + batch_begin_at = 1.week.ago.in_time_zone(TimeZoneUtils.submission_time_zone).beginning_of_week(:monday) + + BatchSubmissionsSelector.weekly_batches(batch_begin_at).each do |batch| + existing_deliveries = batch.submissions.first.deliveries.weekly + if existing_deliveries.any? + Rails.logger.warn("Weekly batch delivery already exists for batch - skipping", { + form_id: batch.form_id, mode: batch.mode, batch_begin_at:, delivery_id: existing_deliveries.first.id + }) + next + end + + delivery = Delivery.create!( + delivery_schedule: :weekly, + submissions: batch.submissions, + batch_begin_at:, + ) + + send_batch_job = SendSubmissionBatchJob.perform_later(delivery:) + + Rails.logger.info("Scheduled SendSubmissionBatchJob to send weekly submission batch", { + form_id: batch.form_id, + mode: batch.mode, + batch_begin_date: batch_begin_at.to_date, + send_submission_batch_job_id: send_batch_job.job_id, + delivery_id: delivery.id, + }) + end + end +end diff --git a/app/lib/batch_submissions_selector.rb b/app/lib/batch_submissions_selector.rb new file mode 100644 index 000000000..99efa7268 --- /dev/null +++ b/app/lib/batch_submissions_selector.rb @@ -0,0 +1,53 @@ +class BatchSubmissionsSelector + Batch = Data.define(:form_id, :mode, :submissions) + + class << self + def daily_batches(date) + Enumerator.new do |yielder| + form_ids_and_modes_with_send_daily_submission_batch(date).each do |form_id, mode| + submissions = Submission.for_form_and_mode(form_id, mode).on_day(date).order(created_at: :desc) + + # If the send_daily_submission_batch is true for the most recent submission, include all submissions on that + # day in the batch. If it is false do not return a batch for any of the submissions on that day. + next unless submissions.any? && submissions.first.form_document["send_daily_submission_batch"] == true + + yielder << Batch.new(form_id, mode, submissions) + end + end + end + + def weekly_batches(time_in_week) + Enumerator.new do |yielder| + form_ids_and_modes_with_send_weekly_submission_batch(time_in_week).each do |form_id, mode| + submissions = Submission.for_form_and_mode(form_id, mode).in_week(time_in_week).order(created_at: :desc) + + # If the send_weekly_submission_batch is true for the most recent submission, include all submissions in that + # week in the batch. If it is false do not return a batch for any of the submissions in that week. + next unless submissions.any? && submissions.first.form_document["send_weekly_submission_batch"] == true + + yielder << Batch.new(form_id, mode, submissions) + end + end + end + + private + + def form_ids_and_modes_with_send_daily_submission_batch(date) + # Get all form_ids and modes that have at least one submission on the date with send_daily_submission_batch + # set to true. + Submission.on_day(date) + .where("(form_document->>'send_daily_submission_batch')::boolean = true") + .distinct + .pluck(:form_id, :mode) + end + + def form_ids_and_modes_with_send_weekly_submission_batch(begin_at) + # Get all form_ids and modes that have at least one submission in the week with send_weekly_submission_batch + # set to true. + Submission.in_week(begin_at) + .where("(form_document->>'send_weekly_submission_batch')::boolean = true") + .distinct + .pluck(:form_id, :mode) + end + end +end diff --git a/app/lib/daily_submission_batch_selector.rb b/app/lib/daily_submission_batch_selector.rb deleted file mode 100644 index 7c075fccc..000000000 --- a/app/lib/daily_submission_batch_selector.rb +++ /dev/null @@ -1,30 +0,0 @@ -class DailySubmissionBatchSelector - Batch = Data.define(:form_id, :mode, :submissions) - - class << self - def batches(date) - Enumerator.new do |yielder| - form_ids_and_modes_with_send_daily_submission_batch(date).each do |form_id, mode| - submissions = Submission.for_form_and_mode(form_id, mode).on_day(date).order(created_at: :desc) - - # If the send_daily_submission_batch is true for the most recent submission, include all submissions on that - # day in the batch. If it is false do not return a batch for any of the submissions on that day. - next unless submissions.any? && submissions.first.form_document["send_daily_submission_batch"] == true - - yielder << Batch.new(form_id, mode, submissions) - end - end - end - - private - - def form_ids_and_modes_with_send_daily_submission_batch(date) - # Get all form_ids and modes that have at least one submission on the date with send_daily_submission_batch - # set to true. - Submission.on_day(date) - .where("(form_document->>'send_daily_submission_batch')::boolean = true") - .distinct - .pluck(:form_id, :mode) - end - end -end diff --git a/app/models/submission.rb b/app/models/submission.rb index 3120c448a..7ad08edce 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -11,6 +11,11 @@ class Submission < ApplicationRecord where(created_at: range) } + scope :in_week, lambda { |time_in_week| + range = time_in_week.in_time_zone(TimeZoneUtils.submission_time_zone).all_week(:monday) + where(created_at: range) + } + scope :ordered_by_form_version_and_date, lambda { order(Arel.sql("(form_document->>'updated_at')::timestamptz ASC, created_at ASC")) } diff --git a/config/recurring.yml b/config/recurring.yml index 28986e8d9..d1e7f7c78 100644 --- a/config/recurring.yml +++ b/config/recurring.yml @@ -14,6 +14,9 @@ production: schedule_daily_batch_deliveries_job: class: ScheduleDailyBatchDeliveriesJob schedule: every day at 2am Europe/London + schedule_weekly_batch_deliveries_job: + class: ScheduleWeeklyBatchDeliveriesJob + schedule: every Monday at 2:15am Europe/London development: delete_submissions_job: class: DeleteSubmissionsJob @@ -30,3 +33,6 @@ development: schedule_daily_batch_deliveries_job: class: ScheduleDailyBatchDeliveriesJob schedule: every day at 2am Europe/London + schedule_weekly_batch_deliveries_job: + class: ScheduleWeeklyBatchDeliveriesJob + schedule: every Monday at 2:15am Europe/London diff --git a/spec/jobs/schedule_daily_batch_deliveries_job_spec.rb b/spec/jobs/schedule_daily_batch_deliveries_job_spec.rb index b49d0331e..71a553036 100644 --- a/spec/jobs/schedule_daily_batch_deliveries_job_spec.rb +++ b/spec/jobs/schedule_daily_batch_deliveries_job_spec.rb @@ -11,8 +11,8 @@ let(:other_form_submissions) { create_list(:submission, 1, form_id: other_form_id, mode: "preview-draft") } let!(:batches) do [ - DailySubmissionBatchSelector::Batch.new(101, "form", form_submissions), - DailySubmissionBatchSelector::Batch.new(201, "preview-draft", other_form_submissions), + BatchSubmissionsSelector::Batch.new(101, "form", form_submissions), + BatchSubmissionsSelector::Batch.new(201, "preview-draft", other_form_submissions), ] end @@ -23,7 +23,7 @@ end before do - allow(DailySubmissionBatchSelector).to receive(:batches).and_return(batches.to_enum) + allow(BatchSubmissionsSelector).to receive(:daily_batches).and_return(batches.to_enum) end context "when Deliveries do not already exist for batches" do @@ -32,7 +32,7 @@ end it "calls the selector with yesterday's date" do - expect(DailySubmissionBatchSelector).to have_received(:batches).with(Time.zone.yesterday) + expect(BatchSubmissionsSelector).to have_received(:daily_batches).with(Time.zone.yesterday) end it "creates a delivery record per batch job" do diff --git a/spec/jobs/schedule_weekly_batch_deliveries_job_spec.rb b/spec/jobs/schedule_weekly_batch_deliveries_job_spec.rb new file mode 100644 index 000000000..d0d222cdb --- /dev/null +++ b/spec/jobs/schedule_weekly_batch_deliveries_job_spec.rb @@ -0,0 +1,132 @@ +require "rails_helper" + +RSpec.describe ScheduleWeeklyBatchDeliveriesJob do + include ActiveSupport::Testing::TimeHelpers + include ActiveJob::TestHelper + + let(:travel_time) { Time.utc(2025, 5, 20, 2, 0, 0) } + let(:form_id) { 101 } + let(:other_form_id) { 201 } + let(:form_submissions) { create_list(:submission, 2, form_id: form_id, mode: "form") } + let(:other_form_submissions) { create_list(:submission, 1, form_id: other_form_id, mode: "preview-draft") } + let!(:batches) do + [ + BatchSubmissionsSelector::Batch.new(101, "form", form_submissions), + BatchSubmissionsSelector::Batch.new(201, "preview-draft", other_form_submissions), + ] + end + + around do |example| + travel_to travel_time do + example.run + end + end + + before do + allow(BatchSubmissionsSelector).to receive(:weekly_batches).and_return(batches.to_enum) + end + + context "when Deliveries do not already exist for batches" do + before do + described_class.perform_now + end + + it "calls the selector passing in the start time of the previous week" do + expect(BatchSubmissionsSelector).to have_received(:weekly_batches).with(Time.utc(2025, 5, 11, 23, 0, 0)) + end + + it "creates a delivery record per batch job" do + expect(Delivery.weekly.count).to eq(2) + expect(Delivery.first.submissions.map(&:id)).to match_array(form_submissions.map(&:id)) + expect(Delivery.second.submissions.map(&:id)).to match_array(other_form_submissions.map(&:id)) + end + + it "enqueues a SendSubmissionBatchJob per batch" do + expect(ActiveJob::Base.queue_adapter.enqueued_jobs.size).to eq(2) + end + + it "enqueues the jobs with the correct args" do + enqueued_args = ActiveJob::Base.queue_adapter.enqueued_jobs.map { |j| j[:args].first } + expect(enqueued_args.first).to include("delivery" => hash_including("_aj_globalid")) + expect(locate_delivery(enqueued_args.first)).to eq(Delivery.first) + + expect(enqueued_args.second).to include("delivery" => hash_including("_aj_globalid")) + expect(locate_delivery(enqueued_args.second)).to eq(Delivery.second) + end + + describe "setting batch_begin_at" do + context "when the week for the batch is the week the clocks go forwards" do + let(:travel_time) { Time.utc(2025, 3, 31, 2, 0, 0) } + + it "sets the batch_begin_at to the beginning of the week in GMT" do + expect(Delivery.first.batch_begin_at).to eq(Time.utc(2025, 3, 24, 0, 0, 0)) + end + end + + context "when the week for the batch is the week after the clocks have gone forwards" do + let(:travel_time) { Time.utc(2025, 4, 7, 2, 0, 0) } + + it "sets the batch_begin_at to the beginning of the week in BST" do + expect(Delivery.first.batch_begin_at).to eq(Time.utc(2025, 3, 30, 23, 0, 0)) + end + end + + context "when the week for the batch is the week the clocks go back" do + let(:travel_time) { Time.zone.local(2025, 10, 27, 2, 0, 0) } + + it "sets the batch_begin_at to the beginning of the week in BST" do + expect(Delivery.first.batch_begin_at).to eq(Time.utc(2025, 10, 19, 23, 0, 0)) + end + end + + context "when the week for the batch is the week after the clocks have gone back" do + let(:travel_time) { Time.utc(2025, 11, 3, 2, 0, 0) } + + it "sets the batch_begin_at to the beginning of the week in GMT" do + expect(Delivery.first.batch_begin_at).to eq(Time.utc(2025, 10, 27, 0, 0, 0)) + end + end + end + end + + context "when a Delivery already exists for a batch" do + let!(:existing_delivery) { create(:delivery, delivery_schedule: :weekly, submissions: form_submissions) } + + it "logs that the delivery will be skipped" do + expect(Rails.logger).to receive(:warn).with( + "Weekly batch delivery already exists for batch - skipping", + hash_including( + form_id: form_id, + mode: "form", + batch_begin_at: Time.utc(2025, 5, 11, 23, 0, 0), + delivery_id: existing_delivery.id, + ), + ) + + described_class.perform_now + end + + it "only creates a delivery for the batch without an existing delivery" do + expect { + described_class.perform_now + }.to change(Delivery, :count).by(1) + + expect(Delivery.last.submissions.map(&:id)).to match_array(other_form_submissions.map(&:id)) + end + + it "only schedules a job for the batch without an existing delivery" do + expect { + described_class.perform_now + }.to change { ActiveJob::Base.queue_adapter.enqueued_jobs.size }.by(1) + + enqueued_args = ActiveJob::Base.queue_adapter.enqueued_jobs.map { |j| j[:args].first } + expect(enqueued_args.first).to include("delivery" => hash_including("_aj_globalid")) + expect(locate_delivery(enqueued_args.first)).to eq(Delivery.last) + end + end + + def locate_delivery(enqueued_args) + gid_string = enqueued_args.dig("delivery", "_aj_globalid") + GlobalID::Locator.locate(gid_string) + end +end diff --git a/spec/lib/batch_submissions_selector_spec.rb b/spec/lib/batch_submissions_selector_spec.rb new file mode 100644 index 000000000..f76abfc1d --- /dev/null +++ b/spec/lib/batch_submissions_selector_spec.rb @@ -0,0 +1,253 @@ +require "rails_helper" + +RSpec.describe BatchSubmissionsSelector do + let(:form_id) { 101 } + + describe ".daily_batches" do + subject(:daily_batches) { described_class.daily_batches(date) } + + let(:date) { Time.zone.local(2022, 12, 1) } + let(:form_document_with_batch_enabled) { create(:v2_form_document, send_daily_submission_batch: true) } + let(:form_document_with_batch_disabled) { create(:v2_form_document, send_daily_submission_batch: false) } + + it "returns an enumerator" do + expect(daily_batches).to be_an(Enumerator) + end + + context "when send_daily_submission_batch is enabled for the form document" do + context "when the date is during BST" do + let(:date) { Time.zone.local(2022, 6, 1) } + + let!(:form_submission) do + create(:submission, form_id: form_id, mode: "form", reference: "INCLUDED1", created_at: Time.utc(2022, 5, 31, 23, 0, 0), form_document: form_document_with_batch_enabled) + end + let!(:preview_draft_submission) do + create(:submission, form_id: form_id, mode: "preview-draft", reference: "INCLUDED2", created_at: Time.utc(2022, 6, 1, 22, 59, 59), form_document: form_document_with_batch_enabled) + end + + before do + # create form/mode combinations that only have submissions outside the BST day + create(:submission, form_id: form_id, mode: "preview-archived", reference: "OMITTED1", created_at: Time.utc(2022, 5, 31, 22, 59, 59), form_document: form_document_with_batch_enabled) + create(:submission, form_id: 102, mode: "form", reference: "OMITTED2", created_at: Time.utc(2022, 6, 1, 23, 0, 0), form_document: form_document_with_batch_enabled) + + # create submissions for the form/mode included in a batch outside the BST day to ensure they are excluded + create(:submission, form_id: form_id, mode: "form", reference: "OMITTED3", created_at: Time.utc(2022, 5, 31, 22, 59, 59), form_document: form_document_with_batch_enabled) + create(:submission, form_id: form_id, mode: "form", reference: "OMITTED4", created_at: Time.utc(2022, 6, 1, 23, 0, 0), form_document: form_document_with_batch_enabled) + end + + it "includes only forms/modes with submissions on the date" do + expect(daily_batches.map(&:to_h)).to contain_exactly( + a_hash_including(form_id: form_id, mode: "form"), + a_hash_including(form_id: form_id, mode: "preview-draft"), + ) + end + + it "includes only submissions on the day in the batches" do + expect(daily_batches.to_a[0].submissions.pluck(:reference)).to contain_exactly(form_submission.reference) + expect(daily_batches.to_a[1].submissions.pluck(:reference)).to contain_exactly(preview_draft_submission.reference) + end + end + + context "when the date is not in BST" do + let(:date) { Time.zone.local(2022, 12, 1) } + + let!(:form_submission) do + create(:submission, form_id: form_id, mode: "form", reference: "INCLUDED1", created_at: Time.utc(2022, 12, 1, 0, 0, 0), form_document: form_document_with_batch_enabled) + end + let!(:preview_draft_submission) do + create(:submission, form_id: form_id, mode: "preview-draft", reference: "INCLUDED2", created_at: Time.utc(2022, 12, 1, 23, 59, 59), form_document: form_document_with_batch_enabled) + end + + before do + # create form/mode combinations that only have submissions outside the day + create(:submission, form_id: form_id, mode: "preview-archived", reference: "OMITTED1", created_at: Time.utc(2022, 11, 30, 23, 59, 59), form_document: form_document_with_batch_enabled) + create(:submission, form_id: 102, mode: "form", reference: "OMITTED2", created_at: Time.utc(2022, 12, 2, 0, 0, 0), form_document: form_document_with_batch_enabled) + + # create submissions for the form/mode included in a batch outside the day to ensure they are excluded + create(:submission, form_id: form_id, mode: "form", reference: "OMITTED3", created_at: Time.utc(2022, 11, 30, 23, 59, 59), form_document: form_document_with_batch_enabled) + create(:submission, form_id: form_id, mode: "form", reference: "OMITTED4", created_at: Time.utc(2022, 12, 2, 0, 0, 0), form_document: form_document_with_batch_enabled) + end + + it "includes only forms/modes with submissions on the date" do + expect(daily_batches.map(&:to_h)).to contain_exactly( + a_hash_including(form_id: form_id, mode: "form"), + a_hash_including(form_id: form_id, mode: "preview-draft"), + ) + end + + it "includes only submissions on the day in the batches" do + expect(daily_batches.to_a[0].submissions.pluck(:reference)).to contain_exactly(form_submission.reference) + expect(daily_batches.to_a[1].submissions.pluck(:reference)).to contain_exactly(preview_draft_submission.reference) + end + end + end + + context "when send_daily_submission_batch is enabled part-way through the day for the form document" do + let!(:latest_submission) do + create(:submission, form_id: form_id, mode: "form", reference: "INCLUDED1", created_at: Time.utc(2022, 12, 1, 10, 0, 0), form_document: form_document_with_batch_enabled) + end + let!(:earlier_submission) do + create(:submission, form_id: form_id, mode: "form", reference: "INCLUDED2", created_at: Time.utc(2022, 12, 1, 9, 0, 0), form_document: form_document_with_batch_disabled) + end + + it "includes a batch for the form and mode" do + expect(daily_batches.map(&:to_h)).to contain_exactly( + a_hash_including(form_id: form_id, mode: "form"), + ) + end + + it "includes all the submissions in the batch" do + submissions = daily_batches.first.submissions + expect(submissions.pluck(:reference)).to contain_exactly(latest_submission.reference, earlier_submission.reference) + end + end + + context "when send_daily_submission_batch is disabled for the form document" do + before do + create(:submission, form_id: form_id, mode: "form", created_at: Time.utc(2022, 12, 1, 10, 0, 0), form_document: form_document_with_batch_disabled) + end + + it "does not include a batch for the form and mode" do + expect(daily_batches.to_a).to be_empty + end + end + + context "when send_daily_submission_batch is disabled part-way through the day for the form document" do + before do + create(:submission, form_id: form_id, mode: "form", created_at: Time.utc(2022, 12, 1, 10, 0, 0), form_document: form_document_with_batch_disabled) + create(:submission, form_id: form_id, mode: "form", created_at: Time.utc(2022, 12, 1, 9, 0, 0), form_document: form_document_with_batch_enabled) + end + + it "does not include a batch for the form and mode" do + expect(daily_batches.to_a).to be_empty + end + end + end + + describe ".weekly_batches" do + subject(:weekly_batches) { described_class.weekly_batches(date) } + + let(:date) { Time.zone.local(2025, 5, 19) } + let(:form_document_with_batch_enabled) { create(:v2_form_document, send_weekly_submission_batch: true) } + let(:form_document_with_batch_disabled) { create(:v2_form_document, send_weekly_submission_batch: false) } + + it "returns an enumerator" do + expect(weekly_batches).to be_an(Enumerator) + end + + context "when send_weekly_submission_batch is enabled for the form document" do + context "when the week is during BST" do + let(:date) { Time.zone.local(2025, 5, 19) } + + let!(:form_submission) do + create(:submission, form_id: form_id, mode: "form", reference: "INCLUDED1", created_at: Time.utc(2025, 5, 18, 23, 0, 0), form_document: form_document_with_batch_enabled) + end + let!(:preview_draft_submission) do + create(:submission, form_id: form_id, mode: "preview-draft", reference: "INCLUDED2", created_at: Time.utc(2025, 5, 25, 22, 59, 59), form_document: form_document_with_batch_enabled) + end + + before do + # create form/mode combinations that only have submissions outside the BST week + create(:submission, form_id: form_id, mode: "preview-archived", reference: "OMITTED1", created_at: Time.utc(2025, 5, 18, 22, 59, 59), form_document: form_document_with_batch_enabled) + create(:submission, form_id: 102, mode: "form", reference: "OMITTED2", created_at: Time.utc(2025, 5, 25, 23, 0, 0), form_document: form_document_with_batch_enabled) + + # create submissions for the form/mode included in a batch outside the BST week to ensure they are excluded + create(:submission, form_id: form_id, mode: "form", reference: "OMITTED3", created_at: Time.utc(2025, 5, 18, 22, 59, 59), form_document: form_document_with_batch_enabled) + create(:submission, form_id: form_id, mode: "form", reference: "OMITTED4", created_at: Time.utc(2025, 5, 25, 23, 0, 0), form_document: form_document_with_batch_enabled) + end + + it "includes only forms/modes with submissions in the week" do + expect(weekly_batches.map(&:to_h)).to contain_exactly( + a_hash_including(form_id: form_id, mode: "form"), + a_hash_including(form_id: form_id, mode: "preview-draft"), + ) + end + + it "includes only submissions in the week in the batches" do + expect(weekly_batches.to_a[0].submissions.pluck(:reference)).to contain_exactly(form_submission.reference) + expect(weekly_batches.to_a[1].submissions.pluck(:reference)).to contain_exactly(preview_draft_submission.reference) + end + end + + context "when the week is not in BST" do + let(:date) { Time.zone.local(2025, 11, 3) } + + let!(:form_submission) do + create(:submission, form_id: form_id, mode: "form", reference: "INCLUDED1", created_at: Time.utc(2025, 11, 3, 0, 0, 0), form_document: form_document_with_batch_enabled) + end + let!(:preview_draft_submission) do + create(:submission, form_id: form_id, mode: "preview-draft", reference: "INCLUDED2", created_at: Time.utc(2025, 11, 9, 23, 59, 59), form_document: form_document_with_batch_enabled) + end + + before do + # create form/mode combinations that only have submissions outside the week + create(:submission, form_id: form_id, mode: "preview-archived", reference: "OMITTED1", created_at: Time.utc(2025, 11, 2, 23, 59, 59), form_document: form_document_with_batch_enabled) + create(:submission, form_id: 102, mode: "form", reference: "OMITTED2", created_at: Time.utc(2025, 11, 10, 0, 0, 0), form_document: form_document_with_batch_enabled) + + # create submissions for the form/mode included in a batch outside the week to ensure they are excluded + create(:submission, form_id: form_id, mode: "form", reference: "OMITTED3", created_at: Time.utc(2025, 11, 2, 23, 59, 59), form_document: form_document_with_batch_enabled) + create(:submission, form_id: form_id, mode: "form", reference: "OMITTED4", created_at: Time.utc(2025, 11, 10, 0, 0, 0), form_document: form_document_with_batch_enabled) + end + + it "includes only forms/modes with submissions in the week" do + expect(weekly_batches.map(&:to_h)).to contain_exactly( + a_hash_including(form_id: form_id, mode: "form"), + a_hash_including(form_id: form_id, mode: "preview-draft"), + ) + end + + it "includes only submissions in the week in the batches" do + expect(weekly_batches.to_a[0].submissions.pluck(:reference)).to contain_exactly(form_submission.reference) + expect(weekly_batches.to_a[1].submissions.pluck(:reference)).to contain_exactly(preview_draft_submission.reference) + end + end + end + + context "when send_weekly_submission_batch is enabled part-way through the week for the form document" do + let(:date) { Time.zone.local(2025, 11, 3) } + + let!(:latest_submission) do + create(:submission, form_id: form_id, mode: "form", reference: "INCLUDED1", created_at: Time.utc(2025, 11, 5, 0, 0, 0), form_document: form_document_with_batch_enabled) + end + let!(:earlier_submission) do + create(:submission, form_id: form_id, mode: "form", reference: "INCLUDED2", created_at: Time.utc(2025, 11, 4, 0, 0, 0), form_document: form_document_with_batch_disabled) + end + + it "includes a batch for the form and mode" do + expect(weekly_batches.map(&:to_h)).to contain_exactly( + a_hash_including(form_id: form_id, mode: "form"), + ) + end + + it "includes all the submissions in the batch" do + submissions = weekly_batches.first.submissions + expect(submissions.pluck(:reference)).to contain_exactly(latest_submission.reference, earlier_submission.reference) + end + end + + context "when send_weekly_submission_batch is disabled for the form document" do + let(:date) { Time.zone.local(2025, 11, 3) } + + before do + create(:submission, form_id: form_id, mode: "form", created_at: Time.utc(2025, 11, 5, 10, 0, 0), form_document: form_document_with_batch_disabled) + end + + it "does not include a batch for the form and mode" do + expect(weekly_batches.to_a).to be_empty + end + end + + context "when send_weekly_submission_batch is disabled part-way through the week for the form document" do + let(:date) { Time.zone.local(2025, 11, 3) } + + before do + create(:submission, form_id: form_id, mode: "form", created_at: Time.utc(2025, 11, 5, 0, 0, 0), form_document: form_document_with_batch_disabled) + create(:submission, form_id: form_id, mode: "form", created_at: Time.utc(2025, 11, 4, 0, 0, 0), form_document: form_document_with_batch_enabled) + end + + it "does not include a batch for the form and mode" do + expect(weekly_batches.to_a).to be_empty + end + end + end +end diff --git a/spec/lib/daily_submission_batch_selector_spec.rb b/spec/lib/daily_submission_batch_selector_spec.rb deleted file mode 100644 index dde2c7b68..000000000 --- a/spec/lib/daily_submission_batch_selector_spec.rb +++ /dev/null @@ -1,128 +0,0 @@ -require "rails_helper" - -RSpec.describe DailySubmissionBatchSelector do - let(:form_document_with_batch_enabled) { create(:v2_form_document, send_daily_submission_batch: true) } - let(:form_document_with_batch_disabled) { create(:v2_form_document, send_daily_submission_batch: false) } - - let(:date) { Time.zone.local(2022, 12, 1) } - let(:form_id) { 101 } - - describe ".batches" do - subject(:batches) { described_class.batches(date) } - - it "returns an enumerator" do - expect(batches).to be_an(Enumerator) - end - - context "when send_daily_submissions_batch is enabled for the form document" do - context "when the date is during BST" do - let(:date) { Time.zone.local(2022, 6, 1) } - - let!(:form_submission) do - create(:submission, form_id: form_id, mode: "form", reference: "INCLUDED1", created_at: Time.utc(2022, 5, 31, 23, 0, 0), form_document: form_document_with_batch_enabled) - end - let!(:preview_draft_submission) do - create(:submission, form_id: form_id, mode: "preview-draft", reference: "INCLUDED2", created_at: Time.utc(2022, 6, 1, 22, 59, 59), form_document: form_document_with_batch_enabled) - end - - before do - # create form/mode combinations that only have submissions outside the BST day - create(:submission, form_id: form_id, mode: "preview-archived", reference: "OMITTED1", created_at: Time.utc(2022, 5, 31, 22, 59, 59), form_document: form_document_with_batch_enabled) - create(:submission, form_id: 102, mode: "form", reference: "OMITTED2", created_at: Time.utc(2022, 6, 1, 23, 0, 0), form_document: form_document_with_batch_enabled) - - # create submissions for the form/mode included in a batch outside the BST day to ensure they are excluded - create(:submission, form_id: form_id, mode: "form", reference: "OMITTED3", created_at: Time.utc(2022, 5, 31, 22, 59, 59), form_document: form_document_with_batch_enabled) - create(:submission, form_id: form_id, mode: "form", reference: "OMITTED4", created_at: Time.utc(2022, 6, 1, 23, 0, 0), form_document: form_document_with_batch_enabled) - end - - it "includes only forms/modes with submissions on the date and their submissions" do - expect(batches.map(&:to_h)).to contain_exactly( - a_hash_including(form_id: form_id, mode: "form"), - a_hash_including(form_id: form_id, mode: "preview-draft"), - ) - end - - it "includes only submissions on the day in the batches" do - expect(batches.to_a[0].submissions.pluck(:reference)).to contain_exactly(form_submission.reference) - expect(batches.to_a[1].submissions.pluck(:reference)).to contain_exactly(preview_draft_submission.reference) - end - end - - context "when the date is not in BST" do - let(:date) { Time.zone.local(2022, 12, 1) } - - let!(:form_submission) do - create(:submission, form_id: form_id, mode: "form", reference: "INCLUDED1", created_at: Time.utc(2022, 12, 1, 0, 0, 0), form_document: form_document_with_batch_enabled) - end - let!(:preview_draft_submission) do - create(:submission, form_id: form_id, mode: "preview-draft", reference: "INCLUDED2", created_at: Time.utc(2022, 12, 1, 23, 59, 59), form_document: form_document_with_batch_enabled) - end - - before do - # create form/mode combinations that only have submissions outside the day - create(:submission, form_id: form_id, mode: "preview-archived", reference: "OMITTED1", created_at: Time.utc(2022, 11, 30, 23, 59, 59), form_document: form_document_with_batch_enabled) - create(:submission, form_id: 102, mode: "form", reference: "OMITTED2", created_at: Time.utc(2022, 12, 2, 0, 0, 0), form_document: form_document_with_batch_enabled) - - # create submissions for the form/mode included in a batch outside the day to ensure they are excluded - create(:submission, form_id: form_id, mode: "form", reference: "OMITTED3", created_at: Time.utc(2022, 11, 30, 23, 59, 59), form_document: form_document_with_batch_enabled) - create(:submission, form_id: form_id, mode: "form", reference: "OMITTED4", created_at: Time.utc(2022, 12, 2, 0, 0, 0), form_document: form_document_with_batch_enabled) - end - - it "includes only forms/modes with submissions on the date" do - expect(batches.map(&:to_h)).to contain_exactly( - a_hash_including(form_id: form_id, mode: "form"), - a_hash_including(form_id: form_id, mode: "preview-draft"), - ) - end - - it "includes only submissions on the day in the batches" do - expect(batches.to_a[0].submissions.pluck(:reference)).to contain_exactly(form_submission.reference) - expect(batches.to_a[1].submissions.pluck(:reference)).to contain_exactly(preview_draft_submission.reference) - end - end - end - - context "when send_daily_submissions_batch is enabled part-way through the day for the form document" do - let(:other_form_id) { 102 } - - let!(:latest_submission) do - create(:submission, form_id: form_id, mode: "form", reference: "INCLUDED1", created_at: Time.utc(2022, 12, 1, 10, 0, 0), form_document: form_document_with_batch_enabled) - end - let!(:earlier_submission) do - create(:submission, form_id: form_id, mode: "form", reference: "INCLUDED2", created_at: Time.utc(2022, 12, 1, 9, 0, 0), form_document: form_document_with_batch_disabled) - end - - it "includes a batch for the form and mode" do - expect(batches.map(&:to_h)).to contain_exactly( - a_hash_including(form_id: form_id, mode: "form"), - ) - end - - it "includes all the submissions in the batch" do - submissions = batches.first.submissions - expect(submissions.pluck(:reference)).to contain_exactly(latest_submission.reference, earlier_submission.reference) - end - end - - context "when send_daily_submissions_batch is disabled for the form document" do - before do - create(:submission, form_id: form_id, mode: "form", created_at: Time.utc(2022, 12, 1, 10, 0, 0), form_document: form_document_with_batch_disabled) - end - - it "does not include a batch for the form and mode" do - expect(batches.to_a).to be_empty - end - end - - context "when send_daily_submissions_batch is disabled part-way through the day for the form document" do - before do - create(:submission, form_id: form_id, mode: "form", created_at: Time.utc(2022, 12, 1, 10, 0, 0), form_document: form_document_with_batch_disabled) - create(:submission, form_id: form_id, mode: "form", created_at: Time.utc(2022, 12, 1, 9, 0, 0), form_document: form_document_with_batch_enabled) - end - - it "does not include a batch for the form and mode" do - expect(batches.to_a).to be_empty - end - end - end -end diff --git a/spec/models/submission_spec.rb b/spec/models/submission_spec.rb index 8bc129a18..1969a9bce 100644 --- a/spec/models/submission_spec.rb +++ b/spec/models/submission_spec.rb @@ -85,6 +85,66 @@ end end + describe ".in_week" do + context "when the date is around the start of BST" do + let!(:gmt_monday_submission) { create(:submission, created_at: Time.utc(2025, 3, 17, 0, 0, 0)) } + let!(:gmt_sunday_submission) { create(:submission, created_at: Time.utc(2025, 3, 23, 23, 59, 59)) } + + let!(:clock_change_week_monday_submission) { create(:submission, created_at: Time.utc(2025, 3, 24, 0, 0, 0)) } + let!(:clock_change_week_sunday_submission) { create(:submission, created_at: Time.utc(2025, 3, 30, 22, 59, 59)) } + + let!(:bst_monday_submission) { create(:submission, created_at: Time.utc(2025, 3, 30, 23, 0, 0)) } + let!(:bst_sunday_submission) { create(:submission, created_at: Time.utc(2025, 4, 6, 22, 59, 59)) } + + it "returns the submissions for the week before the clocks change" do + submissions = described_class.in_week(Time.utc(2025, 3, 17, 0, 0, 0)) + expect(submissions.size).to eq(2) + expect(submissions).to contain_exactly(gmt_monday_submission, gmt_sunday_submission) + end + + it "returns the submissions for the week of the clocks change" do + submissions = described_class.in_week(Time.utc(2025, 3, 24, 0, 0, 0)) + expect(submissions.size).to eq(2) + expect(submissions).to contain_exactly(clock_change_week_monday_submission, clock_change_week_sunday_submission) + end + + it "returns the submissions for the week after the clocks change" do + submissions = described_class.in_week(Time.utc(2025, 3, 30, 23, 0, 0)) + expect(submissions.size).to eq(2) + expect(submissions).to contain_exactly(bst_monday_submission, bst_sunday_submission) + end + end + + context "when the date is around the end of BST" do + let!(:bst_monday_submission) { create(:submission, created_at: Time.utc(2025, 10, 12, 23, 0, 0)) } + let!(:bst_sunday_submission) { create(:submission, created_at: Time.utc(2025, 10, 19, 22, 59, 59)) } + + let!(:clock_change_week_monday_submission) { create(:submission, created_at: Time.utc(2025, 10, 19, 23, 0, 0)) } + let!(:clock_change_week_sunday_submission) { create(:submission, created_at: Time.utc(2025, 10, 26, 23, 59, 59)) } + + let!(:gmt_monday_submission) { create(:submission, created_at: Time.utc(2025, 10, 27, 0, 0, 0)) } + let!(:gmt_sunday_submission) { create(:submission, created_at: Time.utc(2025, 11, 2, 23, 59, 59)) } + + it "returns the submissions for the week before the clocks change" do + submissions = described_class.in_week(Time.utc(2025, 10, 13, 23, 0, 0)) + expect(submissions.size).to eq(2) + expect(submissions).to contain_exactly(bst_monday_submission, bst_sunday_submission) + end + + it "returns the submissions for the week of the clocks change" do + submissions = described_class.in_week(Time.utc(2025, 10, 20, 23, 0, 0)) + expect(submissions.size).to eq(2) + expect(submissions).to contain_exactly(clock_change_week_monday_submission, clock_change_week_sunday_submission) + end + + it "returns the submissions for the week after the clocks change" do + submissions = described_class.in_week(Time.utc(2025, 10, 27, 0, 0, 0)) + expect(submissions.size).to eq(2) + expect(submissions).to contain_exactly(gmt_monday_submission, gmt_sunday_submission) + end + end + end + describe ".ordered_by_form_version_and_date" do let(:first_form_version) { create :v2_form_document, updated_at: Time.utc(2022, 6, 1, 12, 0, 0) } let(:second_form_version) { create :v2_form_document, updated_at: Time.utc(2022, 12, 1, 12, 0, 0) }