Skip to content

Commit 5812bed

Browse files
authored
Merge pull request #1901 from alphagov/add-batched-submissions-job
Add job to send a batch of submissions for a form
2 parents c78bd97 + 29e494e commit 5812bed

15 files changed

+451
-204
lines changed

app/jobs/application_job.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,12 @@ def set_submission_logging_attributes(submission)
77
CurrentJobLoggingAttributes.submission_reference = submission.reference
88
CurrentJobLoggingAttributes.preview = submission.preview?
99
end
10+
11+
def set_submission_batch_logging_attributes(form:, mode:)
12+
CurrentJobLoggingAttributes.job_class = self.class.name
13+
CurrentJobLoggingAttributes.job_id = job_id
14+
CurrentJobLoggingAttributes.form_id = form.id
15+
CurrentJobLoggingAttributes.form_name = form.name
16+
CurrentJobLoggingAttributes.preview = mode.preview?
17+
end
1018
end
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
class SendSubmissionBatchJob < ApplicationJob
2+
queue_as :submissions
3+
4+
# this translates to approximately 4.5 hours of retrying in total
5+
TOTAL_ATTEMPTS = 10
6+
7+
retry_on Aws::SESV2::Errors::ServiceError, wait: :polynomially_longer, attempts: TOTAL_ATTEMPTS
8+
9+
def perform(form_id:, mode_string:, date:, delivery:)
10+
submissions = Submission.for_daily_batch(form_id, date, mode_string)
11+
12+
if submissions.empty?
13+
Rails.logger.info("No submissions to batch for form_id: #{form_id}, mode: #{mode_string}, date: #{date}")
14+
return
15+
end
16+
17+
form = submissions.first.form
18+
mode = Mode.new(mode_string)
19+
set_submission_batch_logging_attributes(form:, mode:)
20+
21+
if form.submission_email.blank?
22+
if mode.preview?
23+
Rails.logger.info "Skipping sending batch for preview submissions, as the submission email address has not been set"
24+
return
25+
else
26+
raise StandardError, "Form id: #{form.id} is missing a submission email address"
27+
end
28+
end
29+
30+
message_id = AwsSesSubmissionBatchService.new(submissions_query: submissions, form:, date:, mode:).send_batch
31+
32+
delivery.update!(
33+
delivery_reference: message_id,
34+
last_attempt_at: Time.zone.now,
35+
submissions: submissions,
36+
)
37+
38+
EventLogger.log_form_event("daily_batch_email_sent", {
39+
mode:,
40+
batch_date: date,
41+
number_of_submissions: submissions.count,
42+
})
43+
44+
submissions.each do |submission|
45+
EventLogger.log_form_event("included_in_daily_batch_email", {
46+
submission_reference: submission.reference,
47+
batch_date: date,
48+
})
49+
end
50+
end
51+
end

app/lib/csv_generator.rb

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,22 @@ def generate_submission(submission:, is_s3_submission:)
99
end
1010
end
1111

12-
def generate_batched_submissions(submissions:, is_s3_submission:)
13-
CSV.generate do |csv|
14-
csv << headers(submissions.first, is_s3_submission)
15-
submissions.each do |submission|
16-
csv << values_for_submission(submission, is_s3_submission)
12+
def generate_batched_submissions(submissions_query:, is_s3_submission:)
13+
sorted_submissions = submissions_query.ordered_by_form_version_and_date
14+
15+
rows_by_version = []
16+
17+
sorted_submissions.each do |submission|
18+
current_headers = rows_by_version.last&.first
19+
unless current_headers == headers(submission, is_s3_submission)
20+
# Start a new CSV if the headers are different to the previous submission
21+
rows_by_version << [headers(submission, is_s3_submission)]
1722
end
23+
rows_by_version.last << values_for_submission(submission, is_s3_submission)
24+
end
25+
26+
rows_by_version.map do |rows|
27+
CSV.generate { |csv| rows.each { |line| csv << line } }
1828
end
1929
end
2030

app/models/submission.rb

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
class Submission < ApplicationRecord
2-
include TimeZoneUtils
3-
42
has_many :submission_deliveries, dependent: :destroy
53
has_many :deliveries, through: :submission_deliveries
64

5+
scope :for_daily_batch, lambda { |form_id, date, mode|
6+
start_time = date.in_time_zone(TimeZoneUtils.submission_time_zone).beginning_of_day
7+
end_time = start_time.end_of_day
8+
9+
where(form_id:, created_at: start_time..end_time, mode: mode)
10+
}
11+
12+
scope :ordered_by_form_version_and_date, lambda {
13+
order(Arel.sql("(form_document->>'updated_at')::timestamptz ASC, created_at ASC"))
14+
}
15+
716
delegate :preview?, to: :mode_object
817

918
encrypts :answers
@@ -17,7 +26,7 @@ def form
1726
end
1827

1928
def submission_time
20-
created_at.in_time_zone(submission_time_zone)
29+
created_at.in_time_zone(TimeZoneUtils.submission_time_zone)
2130
end
2231

2332
def payment_url
@@ -33,25 +42,6 @@ def self.sent?(reference)
3342
submission&.single_submission_delivery&.delivery_reference&.present?
3443
end
3544

36-
def self.group_by_form_version(submissions)
37-
submission_by_version = {}
38-
last_version = nil
39-
40-
# For forms that have the same updated_at timestamp, we know they will be identical. If two forms have different
41-
# updated_at timestamps, we check to see if their steps are the same. If they are, we group those forms' submissions
42-
# together.
43-
submissions.group_by { |submission| submission.form.updated_at }.sort.to_h.each do |updated_at, submissions|
44-
if last_version && last_version.steps == submissions.first.form.steps
45-
submission_by_version[last_version.updated_at].push(*submissions)
46-
else
47-
submission_by_version[updated_at] = submissions
48-
last_version = submissions.first.form
49-
end
50-
end
51-
52-
submission_by_version
53-
end
54-
5545
private
5646

5747
def mode_object

app/services/aws_ses_submission_batch_service.rb

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,19 @@
11
class AwsSesSubmissionBatchService
2-
def initialize(submissions:, form:, date:, mode:)
3-
@submissions = submissions
2+
def initialize(submissions_query:, form:, date:, mode:)
3+
@submissions_query = submissions_query
44
@form = form
55
@date = date
66
@mode = mode
77
end
88

99
def send_batch
10-
if @form.submission_email.blank?
11-
if @mode.preview?
12-
Rails.logger.info "Skipping sending batch for preview submissions, as the submission email address has not been set"
13-
return
14-
else
15-
raise StandardError, "Form id: #{@form.id} is missing a submission email address"
16-
end
17-
end
18-
19-
deliver_batch_email
20-
end
21-
22-
private
23-
24-
def deliver_batch_email
2510
files = {}
2611

27-
submissions_by_version = Submission.group_by_form_version(@submissions)
28-
submissions_by_version.each_value.with_index(1) do |submissions, version_number|
29-
form_version = submissions_by_version.size > 1 ? version_number : nil
30-
filename = SubmissionFilenameGenerator.batch_csv_filename(form_name: @form.name, date: @date, mode: @mode, form_version: form_version)
31-
files[filename] = CsvGenerator.generate_batched_submissions(submissions: submissions, is_s3_submission: false)
12+
csvs = CsvGenerator.generate_batched_submissions(submissions_query: @submissions_query, is_s3_submission: false)
13+
csvs.each.with_index(1) do |csv, index|
14+
csv_version = csvs.size > 1 ? index : nil
15+
filename = SubmissionFilenameGenerator.batch_csv_filename(form_name: @form.name, date: @date, mode: @mode, form_version: csv_version)
16+
files[filename] = csv
3217
end
3318

3419
mail = AwsSesSubmissionBatchMailer.batch_submission_email(form: @form, date: @date, mode: @mode, files:).deliver_now
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class AddIndexOnCreatedAtAndFormIdAndModeToSubmissions < ActiveRecord::Migration[8.1]
2+
def change
3+
add_index :submissions, %i[created_at form_id mode]
4+
end
5+
end

db/schema.rb

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/time_zone_utils.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
module TimeZoneUtils
2-
def submission_time_zone
2+
def self.submission_time_zone
33
Rails.configuration.x.submission.time_zone || "UTC"
44
end
55
end

spec/factories/submissions.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
},
1515
}
1616
end
17-
mode { is_preview ? "preview-live" : "live" }
17+
mode { is_preview ? "preview-live" : "form" }
1818
form_document { build :v2_form_document, form_id: }
1919
submission_locale { :en }
2020

spec/factories/v2_form_document.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
end
3333

3434
steps do
35-
Array.new(steps_count) { attributes_for(:v2_step) }
35+
Array.new(steps_count) { attributes_for(:v2_question_page_step) }
3636
end
3737

3838
question_section_completed { true }

0 commit comments

Comments
 (0)