Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 25 additions & 33 deletions app/services/reporting/irs_credential_tenure_report.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 13 additions & 0 deletions lib/reporting/irs_fraud_metrics_lg99_report.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions spec/jobs/reports/irs_fraud_metrics_report_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"#{report_folder}/definitions.csv",
"#{report_folder}/overview.csv",
"#{report_folder}/lg99_metrics.csv",
"#{report_folder}/Credential_Tenure_Metric.csv",
]
end

Expand All @@ -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'] }

Expand All @@ -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
Expand Down
19 changes: 19 additions & 0 deletions spec/lib/reporting/irs_fraud_metrics_lg99_report_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:) }

Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand Down