diff --git a/app/services/reporting/irs_credential_tenure_report.rb b/app/services/reporting/irs_credential_tenure_report.rb index ea06cddcb11..30d21b8dfa2 100644 --- a/app/services/reporting/irs_credential_tenure_report.rb +++ b/app/services/reporting/irs_credential_tenure_report.rb @@ -73,40 +73,32 @@ def total_user_count def average_credential_tenure_months end_of_month = report_date.end_of_month - - # Efficiently load only created_at timestamps for IRS users - created_ats = User - .joins(:identities) - .where('users.created_at <= ?', end_of_month) - .where(identities: { service_provider: issuers, deleted_at: nil }) - .distinct - .pluck(:created_at) - - return 0 if created_ats.empty? - - total_months = created_ats.sum do |created_at| - precise_months_between(created_at.to_date, end_of_month) + Reports::BaseReport.transaction_with_timeout do + average_months = User + .where( + '(users.confirmed_at <= :end_of_month AND users.suspended_at IS NULL) + OR (users.suspended_at IS NOT NULL AND users.reinstated_at IS NOT NULL)', + end_of_month: end_of_month, + ).where( + 'EXISTS ( + SELECT 1 FROM identities + WHERE identities.user_id = users.id + AND identities.service_provider IN (:issuers) + AND identities.deleted_at IS NULL + )', + issuers: issuers, + ).pick( + Arel.sql( + 'AVG( + EXTRACT(YEAR FROM age(?, users.confirmed_at)) * 12 + + EXTRACT(MONTH FROM age(?, users.confirmed_at)) + + EXTRACT(DAY FROM age(?, users.confirmed_at)) / 30.0 + )', + end_of_month, end_of_month, end_of_month + ), + ).to_f.round(2) + return average_months end - - (total_months.to_f / created_ats.size).round(2) - end - - private - - def precise_months_between(start_date, end_date) - return 0 if end_date < start_date - - # Full months difference - months = (end_date.year - start_date.year) * 12 + (end_date.month - start_date.month) - - # Adjust for partial month - partial_start_day = [start_date.day, Date.new(end_date.year, end_date.month, -1).day].min - partial_start_date = Date.new(end_date.year, end_date.month, partial_start_day) - - day_diff = (end_date - partial_start_date).to_f - days_in_month = Date.new(end_date.year, end_date.month, -1).day.to_f - - months + (day_diff / days_in_month) end end end diff --git a/lib/reporting/irs_fraud_metrics_lg99_report.rb b/lib/reporting/irs_fraud_metrics_lg99_report.rb index 14097186e35..0fd15b274b6 100644 --- a/lib/reporting/irs_fraud_metrics_lg99_report.rb +++ b/lib/reporting/irs_fraud_metrics_lg99_report.rb @@ -69,6 +69,11 @@ def as_emailable_reports table: lg99_metrics_table, filename: 'lg99_metrics', ), + Reporting::EmailableReport.new( + title: "IRS Credential Tenure Metric #{stats_month}", + table: credential_tenure_report_metric, + filename: 'Credential_Tenure_Metric', + ), ] end @@ -83,6 +88,7 @@ def definitions_table ['Credentials Reinstated', 'Count', 'The count of unique suspended accounts ' + ' that are reinstated within the reporting month.'], + ['Credential Tenure', 'Count', 'The average age, in months, of all accounts'], ] end @@ -120,6 +126,13 @@ def lg99_metrics_table ] end + def credential_tenure_report_metric + Reporting::IrsCredentialTenureReport.new( + time_range.end, + issuers: issuers, + ).irs_credential_tenure_report + end + def stats_month time_range.begin.strftime('%b-%Y') end diff --git a/spec/jobs/reports/irs_fraud_metrics_report_spec.rb b/spec/jobs/reports/irs_fraud_metrics_report_spec.rb index a26d0f87846..1cb6dbe7626 100644 --- a/spec/jobs/reports/irs_fraud_metrics_report_spec.rb +++ b/spec/jobs/reports/irs_fraud_metrics_report_spec.rb @@ -16,6 +16,7 @@ "#{report_folder}/definitions.csv", "#{report_folder}/overview.csv", "#{report_folder}/lg99_metrics.csv", + "#{report_folder}/Credential_Tenure_Metric.csv", ] end @@ -39,6 +40,14 @@ ] end + let(:mock_credential_tenure_metric) do + [ + ['Metric', 'Value'], + ['Total Users', 5], + ['Credential Tenure', 2], + ] + end + let(:mock_test_fraud_emails) { ['mock_feds@example.com', 'mock_contractors@example.com'] } let(:mock_test_fraud_issuers) { ['issuer1'] } @@ -60,6 +69,9 @@ allow(report.irs_fraud_metrics_lg99_report).to receive(:lg99_metrics_table) .and_return(mock_identity_verification_lg99_data) + + allow(report.irs_fraud_metrics_lg99_report).to receive(:credential_tenure_report_metric) + .and_return(mock_credential_tenure_metric) end it 'sends out a report to just to team data' do diff --git a/spec/lib/reporting/irs_fraud_metrics_lg99_report_spec.rb b/spec/lib/reporting/irs_fraud_metrics_lg99_report_spec.rb index a536d766b5a..2996041eed4 100644 --- a/spec/lib/reporting/irs_fraud_metrics_lg99_report_spec.rb +++ b/spec/lib/reporting/irs_fraud_metrics_lg99_report_spec.rb @@ -15,6 +15,7 @@ ['Credentials Reinstated', 'Count', 'The count of unique suspended accounts ' + ' that are reinstated within the reporting month.'], + ['Credential Tenure', 'Count', 'The average age, in months, of all accounts'], ] end let(:expected_overview_table) do @@ -33,6 +34,13 @@ ['Credentials Reinstated', '1', time_range.begin.to_s, time_range.end.to_s], ] end + let(:expected_credential_tenure_metric) do + [ + ['Metric', 'Values'], + ['Total Users', '10'], + ['Credential Tenure', '15'], + ] + end subject(:report) { Reporting::IrsFraudMetricsLg99Report.new(issuers: [issuer], time_range:) } @@ -154,6 +162,12 @@ end describe '#as_emailable_reports' do + before do + allow_any_instance_of(Reporting::IrsCredentialTenureReport) + .to receive(:irs_credential_tenure_report) + .and_return(expected_credential_tenure_metric) + end + let(:expected_reports) do [ Reporting::EmailableReport.new( @@ -171,6 +185,11 @@ filename: 'lg99_metrics', table: expected_lg99_metrics_table, ), + Reporting::EmailableReport.new( + title: 'IRS Credential Tenure Metric Jan-2022', + table: expected_credential_tenure_metric, + filename: 'Credential_Tenure_Metric', + ), ] end it 'return expected table for email' do