diff --git a/app/jobs/reports/total_ial2_costs_report.rb b/app/jobs/reports/total_ial2_costs_report.rb new file mode 100644 index 00000000000..9219908761a --- /dev/null +++ b/app/jobs/reports/total_ial2_costs_report.rb @@ -0,0 +1,71 @@ +require 'csv' + +module Reports + class TotalIal2CostsReport < BaseReport + REPORT_NAME = 'total-ial2-costs'.freeze + NUM_LOOKBACK_DAYS = 45 + + include GoodJob::ActiveJobExtensions::Concurrency + + good_job_control_concurrency_with( + total_limit: 1, + key: -> { "#{REPORT_NAME}-#{arguments.first}" }, + ) + + def perform(date) + results = transaction_with_timeout { query(date) } + + save_report(REPORT_NAME, to_csv(results), extension: 'csv') + end + + # @param [PG::Result] + # @return [String] + def to_csv(results) + CSV.generate do |csv| + csv << %w[ + date + ial + cost_type + count + ] + + results.each do |row| + csv << row.values_at('date', 'ial', 'cost_type', 'count') + end + end + end + + # @return [PG::Result] + def query(date) + finish = date.beginning_of_day + start = (finish - NUM_LOOKBACK_DAYS.days).beginning_of_day + + params = { + start: ActiveRecord::Base.connection.quote(start), + finish: ActiveRecord::Base.connection.quote(finish), + } + + sql = format(<<~SQL, params) + SELECT + DATE(sp_costs.created_at) AS date + , sp_costs.ial + , sp_costs.cost_type + , COUNT(*) AS count + FROM sp_costs + WHERE + %{start} <= sp_costs.created_at AND sp_costs.created_at <= %{finish} + AND sp_costs.ial > 1 + GROUP BY + sp_costs.ial + , sp_costs.cost_type + , DATE(sp_costs.created_at) + ORDER BY + sp_costs.ial + , sp_costs.cost_type + , DATE(sp_costs.created_at) + SQL + + ActiveRecord::Base.connection.execute(sql) + end + end +end diff --git a/config/initializers/job_configurations.rb b/config/initializers/job_configurations.rb index bd4cc0ac2fd..99432c9ecf4 100644 --- a/config/initializers/job_configurations.rb +++ b/config/initializers/job_configurations.rb @@ -117,6 +117,12 @@ cron: cron_24h, args: -> { [Time.zone.today] }, }, + # Total IAL2 Costs Report to S3 + total_ial2_costs: { + class: 'Reports::TotalIal2CostsReport', + cron: cron_24h, + args: -> { [Time.zone.today] }, + }, # SP Active Users Report to S3 sp_active_users_report: { class: 'Reports::SpActiveUsersReport', diff --git a/spec/jobs/reports/total_ial2_costs_report_spec.rb b/spec/jobs/reports/total_ial2_costs_report_spec.rb new file mode 100644 index 00000000000..751c32fc192 --- /dev/null +++ b/spec/jobs/reports/total_ial2_costs_report_spec.rb @@ -0,0 +1,81 @@ +require 'rails_helper' + +RSpec.describe Reports::TotalIal2CostsReport do + subject(:report) { described_class.new } + + describe '#perform' do + let(:issuer1) { 'issuer1' } + let(:issuer2) { 'issuer2' } + + let!(:sp1) { create(:service_provider, issuer: issuer1, friendly_name: issuer1) } + let!(:sp2) { create(:service_provider, issuer: issuer2, friendly_name: issuer2) } + + let(:date) { Date.new(2022, 4, 1) } + let(:yesterday) { Date.new(2022, 3, 31) } + let(:yesterday_utc) { yesterday.in_time_zone('UTC') } + let(:too_old) { Date.new(2021, 12, 31) } + + before do + SpCost.create( + agency_id: 1, + issuer: issuer1, + cost_type: 'authentication', + created_at: yesterday_utc, + ial: 2, + ) + SpCost.create( + agency_id: 2, + issuer: issuer2, + cost_type: 'authentication', + created_at: yesterday_utc, + ial: 2, + ) + + SpCost.create( + agency_id: 1, issuer: issuer1, cost_type: 'sms', created_at: yesterday_utc, ial: 2, + ) + + # rows that get ignored + SpCost.create( + agency_id: 2, issuer: issuer2, cost_type: 'user_added', created_at: too_old, ial: 2, + ) + SpCost.create( + agency_id: 2, issuer: issuer2, cost_type: 'user_added', created_at: yesterday_utc, ial: 1, + ) + end + + it 'writes a CSV report to S3' do + expect(report).to receive(:save_report) do |report_name, body, extension:| + expect(report_name).to eq(described_class::REPORT_NAME) + expect(extension).to eq('csv') + + csv = CSV.parse(body, headers: true) + expect(csv.length).to eq(2) + + row = csv.first + expect(row['date']).to eq(yesterday.to_s) + expect(row['cost_type']).to eq('authentication') + expect(row['ial']).to eq(2.to_s) + expect(row['count']).to eq(2.to_s) + + row = csv[1] + expect(row['date']).to eq(yesterday.to_s) + expect(row['cost_type']).to eq('sms') + expect(row['ial']).to eq(2.to_s) + expect(row['count']).to eq(1.to_s) + end + + report.perform(date) + end + end + + describe '#good_job_concurrency_key' do + let(:date) { Time.zone.today } + + it 'is the job name and the date' do + job = described_class.new(date) + expect(job.good_job_concurrency_key). + to eq("#{described_class::REPORT_NAME}-#{date}") + end + end +end