Skip to content

Commit e750a29

Browse files
authored
Merge pull request #1978 from alphagov/add-job-to-schedule-weekly-deliveries
Add job to schedule weekly deliveries
2 parents 74cc489 + b88eb1c commit e750a29

11 files changed

+552
-163
lines changed

app/jobs/schedule_daily_batch_deliveries_job.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def perform
1010
date = Time.zone.yesterday
1111
batch_begin_at = date.in_time_zone(TimeZoneUtils.submission_time_zone).beginning_of_day
1212

13-
DailySubmissionBatchSelector.batches(date).each do |batch|
13+
BatchSubmissionsSelector.daily_batches(date).each do |batch|
1414
existing_deliveries = batch.submissions.first.deliveries.daily
1515
if existing_deliveries.any?
1616
Rails.logger.warn("Daily batch delivery already exists for batch - skipping", {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
class ScheduleWeeklyBatchDeliveriesJob < ApplicationJob
2+
# If we change the queue for this job, ensure we add a new alert in CloudWatch for failed executions
3+
queue_as :submissions
4+
5+
def perform
6+
CloudWatchService.record_job_started_metric(self.class.name)
7+
CurrentJobLoggingAttributes.job_class = self.class.name
8+
CurrentJobLoggingAttributes.job_id = job_id
9+
10+
batch_begin_at = 1.week.ago.in_time_zone(TimeZoneUtils.submission_time_zone).beginning_of_week(:monday)
11+
12+
BatchSubmissionsSelector.weekly_batches(batch_begin_at).each do |batch|
13+
existing_deliveries = batch.submissions.first.deliveries.weekly
14+
if existing_deliveries.any?
15+
Rails.logger.warn("Weekly batch delivery already exists for batch - skipping", {
16+
form_id: batch.form_id, mode: batch.mode, batch_begin_at:, delivery_id: existing_deliveries.first.id
17+
})
18+
next
19+
end
20+
21+
delivery = Delivery.create!(
22+
delivery_schedule: :weekly,
23+
submissions: batch.submissions,
24+
batch_begin_at:,
25+
)
26+
27+
send_batch_job = SendSubmissionBatchJob.perform_later(delivery:)
28+
29+
Rails.logger.info("Scheduled SendSubmissionBatchJob to send weekly submission batch", {
30+
form_id: batch.form_id,
31+
mode: batch.mode,
32+
batch_begin_date: batch_begin_at.to_date,
33+
send_submission_batch_job_id: send_batch_job.job_id,
34+
delivery_id: delivery.id,
35+
})
36+
end
37+
end
38+
end
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
class BatchSubmissionsSelector
2+
Batch = Data.define(:form_id, :mode, :submissions)
3+
4+
class << self
5+
def daily_batches(date)
6+
Enumerator.new do |yielder|
7+
form_ids_and_modes_with_send_daily_submission_batch(date).each do |form_id, mode|
8+
submissions = Submission.for_form_and_mode(form_id, mode).on_day(date).order(created_at: :desc)
9+
10+
# If the send_daily_submission_batch is true for the most recent submission, include all submissions on that
11+
# day in the batch. If it is false do not return a batch for any of the submissions on that day.
12+
next unless submissions.any? && submissions.first.form_document["send_daily_submission_batch"] == true
13+
14+
yielder << Batch.new(form_id, mode, submissions)
15+
end
16+
end
17+
end
18+
19+
def weekly_batches(time_in_week)
20+
Enumerator.new do |yielder|
21+
form_ids_and_modes_with_send_weekly_submission_batch(time_in_week).each do |form_id, mode|
22+
submissions = Submission.for_form_and_mode(form_id, mode).in_week(time_in_week).order(created_at: :desc)
23+
24+
# If the send_weekly_submission_batch is true for the most recent submission, include all submissions in that
25+
# week in the batch. If it is false do not return a batch for any of the submissions in that week.
26+
next unless submissions.any? && submissions.first.form_document["send_weekly_submission_batch"] == true
27+
28+
yielder << Batch.new(form_id, mode, submissions)
29+
end
30+
end
31+
end
32+
33+
private
34+
35+
def form_ids_and_modes_with_send_daily_submission_batch(date)
36+
# Get all form_ids and modes that have at least one submission on the date with send_daily_submission_batch
37+
# set to true.
38+
Submission.on_day(date)
39+
.where("(form_document->>'send_daily_submission_batch')::boolean = true")
40+
.distinct
41+
.pluck(:form_id, :mode)
42+
end
43+
44+
def form_ids_and_modes_with_send_weekly_submission_batch(begin_at)
45+
# Get all form_ids and modes that have at least one submission in the week with send_weekly_submission_batch
46+
# set to true.
47+
Submission.in_week(begin_at)
48+
.where("(form_document->>'send_weekly_submission_batch')::boolean = true")
49+
.distinct
50+
.pluck(:form_id, :mode)
51+
end
52+
end
53+
end

app/lib/daily_submission_batch_selector.rb

Lines changed: 0 additions & 30 deletions
This file was deleted.

app/models/submission.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ class Submission < ApplicationRecord
1111
where(created_at: range)
1212
}
1313

14+
scope :in_week, lambda { |time_in_week|
15+
range = time_in_week.in_time_zone(TimeZoneUtils.submission_time_zone).all_week(:monday)
16+
where(created_at: range)
17+
}
18+
1419
scope :ordered_by_form_version_and_date, lambda {
1520
order(Arel.sql("(form_document->>'updated_at')::timestamptz ASC, created_at ASC"))
1621
}

config/recurring.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ production:
1414
schedule_daily_batch_deliveries_job:
1515
class: ScheduleDailyBatchDeliveriesJob
1616
schedule: every day at 2am Europe/London
17+
schedule_weekly_batch_deliveries_job:
18+
class: ScheduleWeeklyBatchDeliveriesJob
19+
schedule: every Monday at 2:15am Europe/London
1720
development:
1821
delete_submissions_job:
1922
class: DeleteSubmissionsJob
@@ -30,3 +33,6 @@ development:
3033
schedule_daily_batch_deliveries_job:
3134
class: ScheduleDailyBatchDeliveriesJob
3235
schedule: every day at 2am Europe/London
36+
schedule_weekly_batch_deliveries_job:
37+
class: ScheduleWeeklyBatchDeliveriesJob
38+
schedule: every Monday at 2:15am Europe/London

spec/jobs/schedule_daily_batch_deliveries_job_spec.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
let(:other_form_submissions) { create_list(:submission, 1, form_id: other_form_id, mode: "preview-draft") }
1212
let!(:batches) do
1313
[
14-
DailySubmissionBatchSelector::Batch.new(101, "form", form_submissions),
15-
DailySubmissionBatchSelector::Batch.new(201, "preview-draft", other_form_submissions),
14+
BatchSubmissionsSelector::Batch.new(101, "form", form_submissions),
15+
BatchSubmissionsSelector::Batch.new(201, "preview-draft", other_form_submissions),
1616
]
1717
end
1818

@@ -23,7 +23,7 @@
2323
end
2424

2525
before do
26-
allow(DailySubmissionBatchSelector).to receive(:batches).and_return(batches.to_enum)
26+
allow(BatchSubmissionsSelector).to receive(:daily_batches).and_return(batches.to_enum)
2727
end
2828

2929
context "when Deliveries do not already exist for batches" do
@@ -32,7 +32,7 @@
3232
end
3333

3434
it "calls the selector with yesterday's date" do
35-
expect(DailySubmissionBatchSelector).to have_received(:batches).with(Time.zone.yesterday)
35+
expect(BatchSubmissionsSelector).to have_received(:daily_batches).with(Time.zone.yesterday)
3636
end
3737

3838
it "creates a delivery record per batch job" do
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
require "rails_helper"
2+
3+
RSpec.describe ScheduleWeeklyBatchDeliveriesJob do
4+
include ActiveSupport::Testing::TimeHelpers
5+
include ActiveJob::TestHelper
6+
7+
let(:travel_time) { Time.utc(2025, 5, 20, 2, 0, 0) }
8+
let(:form_id) { 101 }
9+
let(:other_form_id) { 201 }
10+
let(:form_submissions) { create_list(:submission, 2, form_id: form_id, mode: "form") }
11+
let(:other_form_submissions) { create_list(:submission, 1, form_id: other_form_id, mode: "preview-draft") }
12+
let!(:batches) do
13+
[
14+
BatchSubmissionsSelector::Batch.new(101, "form", form_submissions),
15+
BatchSubmissionsSelector::Batch.new(201, "preview-draft", other_form_submissions),
16+
]
17+
end
18+
19+
around do |example|
20+
travel_to travel_time do
21+
example.run
22+
end
23+
end
24+
25+
before do
26+
allow(BatchSubmissionsSelector).to receive(:weekly_batches).and_return(batches.to_enum)
27+
end
28+
29+
context "when Deliveries do not already exist for batches" do
30+
before do
31+
described_class.perform_now
32+
end
33+
34+
it "calls the selector passing in the start time of the previous week" do
35+
expect(BatchSubmissionsSelector).to have_received(:weekly_batches).with(Time.utc(2025, 5, 11, 23, 0, 0))
36+
end
37+
38+
it "creates a delivery record per batch job" do
39+
expect(Delivery.weekly.count).to eq(2)
40+
expect(Delivery.first.submissions.map(&:id)).to match_array(form_submissions.map(&:id))
41+
expect(Delivery.second.submissions.map(&:id)).to match_array(other_form_submissions.map(&:id))
42+
end
43+
44+
it "enqueues a SendSubmissionBatchJob per batch" do
45+
expect(ActiveJob::Base.queue_adapter.enqueued_jobs.size).to eq(2)
46+
end
47+
48+
it "enqueues the jobs with the correct args" do
49+
enqueued_args = ActiveJob::Base.queue_adapter.enqueued_jobs.map { |j| j[:args].first }
50+
expect(enqueued_args.first).to include("delivery" => hash_including("_aj_globalid"))
51+
expect(locate_delivery(enqueued_args.first)).to eq(Delivery.first)
52+
53+
expect(enqueued_args.second).to include("delivery" => hash_including("_aj_globalid"))
54+
expect(locate_delivery(enqueued_args.second)).to eq(Delivery.second)
55+
end
56+
57+
describe "setting batch_begin_at" do
58+
context "when the week for the batch is the week the clocks go forwards" do
59+
let(:travel_time) { Time.utc(2025, 3, 31, 2, 0, 0) }
60+
61+
it "sets the batch_begin_at to the beginning of the week in GMT" do
62+
expect(Delivery.first.batch_begin_at).to eq(Time.utc(2025, 3, 24, 0, 0, 0))
63+
end
64+
end
65+
66+
context "when the week for the batch is the week after the clocks have gone forwards" do
67+
let(:travel_time) { Time.utc(2025, 4, 7, 2, 0, 0) }
68+
69+
it "sets the batch_begin_at to the beginning of the week in BST" do
70+
expect(Delivery.first.batch_begin_at).to eq(Time.utc(2025, 3, 30, 23, 0, 0))
71+
end
72+
end
73+
74+
context "when the week for the batch is the week the clocks go back" do
75+
let(:travel_time) { Time.zone.local(2025, 10, 27, 2, 0, 0) }
76+
77+
it "sets the batch_begin_at to the beginning of the week in BST" do
78+
expect(Delivery.first.batch_begin_at).to eq(Time.utc(2025, 10, 19, 23, 0, 0))
79+
end
80+
end
81+
82+
context "when the week for the batch is the week after the clocks have gone back" do
83+
let(:travel_time) { Time.utc(2025, 11, 3, 2, 0, 0) }
84+
85+
it "sets the batch_begin_at to the beginning of the week in GMT" do
86+
expect(Delivery.first.batch_begin_at).to eq(Time.utc(2025, 10, 27, 0, 0, 0))
87+
end
88+
end
89+
end
90+
end
91+
92+
context "when a Delivery already exists for a batch" do
93+
let!(:existing_delivery) { create(:delivery, delivery_schedule: :weekly, submissions: form_submissions) }
94+
95+
it "logs that the delivery will be skipped" do
96+
expect(Rails.logger).to receive(:warn).with(
97+
"Weekly batch delivery already exists for batch - skipping",
98+
hash_including(
99+
form_id: form_id,
100+
mode: "form",
101+
batch_begin_at: Time.utc(2025, 5, 11, 23, 0, 0),
102+
delivery_id: existing_delivery.id,
103+
),
104+
)
105+
106+
described_class.perform_now
107+
end
108+
109+
it "only creates a delivery for the batch without an existing delivery" do
110+
expect {
111+
described_class.perform_now
112+
}.to change(Delivery, :count).by(1)
113+
114+
expect(Delivery.last.submissions.map(&:id)).to match_array(other_form_submissions.map(&:id))
115+
end
116+
117+
it "only schedules a job for the batch without an existing delivery" do
118+
expect {
119+
described_class.perform_now
120+
}.to change { ActiveJob::Base.queue_adapter.enqueued_jobs.size }.by(1)
121+
122+
enqueued_args = ActiveJob::Base.queue_adapter.enqueued_jobs.map { |j| j[:args].first }
123+
expect(enqueued_args.first).to include("delivery" => hash_including("_aj_globalid"))
124+
expect(locate_delivery(enqueued_args.first)).to eq(Delivery.last)
125+
end
126+
end
127+
128+
def locate_delivery(enqueued_args)
129+
gid_string = enqueued_args.dig("delivery", "_aj_globalid")
130+
GlobalID::Locator.locate(gid_string)
131+
end
132+
end

0 commit comments

Comments
 (0)