Class: AccountDailyActivityReport

Inherits:
Object
  • Object
show all
Defined in:
app/services/account_daily_activity_report.rb

Overview

Reporting window for the sysadmin email:

  • Production default: previous calendar day in America/Chicago.
  • Optional local/test: set ACCOUNT_ACTIVITY_REPORT_LOOKBACK_HOURS=48 for a rolling window
    ending at “now” in Chicago (e.g. dev DB snapshot testing).

Constant Summary collapse

TZ =

Tz.

'America/Chicago'
ENV_LOOKBACK_HOURS =

Env lookback hours.

'ACCOUNT_ACTIVITY_REPORT_LOOKBACK_HOURS'

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(date: nil, range: nil, rolling_hours: nil) ⇒ void

Builds a report for either a single calendar date or an explicit range.

Parameters:

  • date (Date, nil) (defaults to: nil)

    calendar day to report on (America/Chicago)

  • range (Range, nil) (defaults to: nil)

    explicit time window (takes precedence over date)

  • rolling_hours (Integer, nil) (defaults to: nil)

    lookback hours when range is a rolling window

Raises:

  • (ArgumentError)

    when neither date: nor range: is given



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'app/services/account_daily_activity_report.rb', line 21

def initialize(date: nil, range: nil, rolling_hours: nil)
  tz = ActiveSupport::TimeZone[TZ]
  if range
    @window_start = range.begin.in_time_zone(tz)
    @window_end = range.end.in_time_zone(tz)
    @date = @window_start.to_date
    @rolling_hours = rolling_hours
  elsif date
    @date = date
    @window_start = tz.local(date.year, date.month, date.day).beginning_of_day
    @window_end = @window_start.end_of_day
    @rolling_hours = nil
  else
    raise ArgumentError, 'Provide date: or range:'
  end
end

Instance Attribute Details

#dateObject (readonly)

Returns the value of attribute date.



13
14
15
# File 'app/services/account_daily_activity_report.rb', line 13

def date
  @date
end

#rolling_hoursObject (readonly)

Returns the value of attribute rolling_hours.



13
14
15
# File 'app/services/account_daily_activity_report.rb', line 13

def rolling_hours
  @rolling_hours
end

#window_endObject (readonly)

Returns the value of attribute window_end.



13
14
15
# File 'app/services/account_daily_activity_report.rb', line 13

def window_end
  @window_end
end

#window_startObject (readonly)

Returns the value of attribute window_start.



13
14
15
# File 'app/services/account_daily_activity_report.rb', line 13

def window_start
  @window_start
end

Class Method Details

.calendar_yesterdayAccountDailyActivityReport

Report over the previous calendar day in America/Chicago.



61
62
63
64
# File 'app/services/account_daily_activity_report.rb', line 61

def self.calendar_yesterday
  tz = ActiveSupport::TimeZone[TZ]
  new(date: tz.now.to_date - 1)
end

.for_scheduled_runAccountDailyActivityReport

Report for the scheduled daily run: a rolling window when
ACCOUNT_ACTIVITY_REPORT_LOOKBACK_HOURS is set, otherwise calendar yesterday.



41
42
43
44
45
46
47
48
# File 'app/services/account_daily_activity_report.rb', line 41

def self.for_scheduled_run
  hours = lookback_hours_from_env
  if hours&.positive?
    rolling_hours(hours)
  else
    calendar_yesterday
  end
end

.lookback_hours_from_envInteger?

Lookback window from the ACCOUNT_ACTIVITY_REPORT_LOOKBACK_HOURS env var.

Returns:

  • (Integer, nil)

    parsed hours, or nil when unset/invalid



68
69
70
71
72
73
74
75
# File 'app/services/account_daily_activity_report.rb', line 68

def self.lookback_hours_from_env
  v = ENV[ENV_LOOKBACK_HOURS].to_s.strip
  return nil if v.blank?

  Integer(v)
rescue ArgumentError, TypeError
  nil
end

.rolling_hours(hours) ⇒ AccountDailyActivityReport

Report over a rolling window of the last hours hours ending now.

Parameters:

  • hours (Integer)

    lookback window length in hours

Returns:



53
54
55
56
57
# File 'app/services/account_daily_activity_report.rb', line 53

def self.rolling_hours(hours)
  tz = ActiveSupport::TimeZone[TZ]
  now = tz.now
  new(range: (now - hours.hours)..now, rolling_hours: hours)
end

Instance Method Details

#accounts_createdActiveRecord::Relation<Account>

Accounts created inside the reporting window, oldest first.

Returns:

  • (ActiveRecord::Relation<Account>)


79
80
81
# File 'app/services/account_daily_activity_report.rb', line 79

def accounts_created
  Account.includes(:party).where(created_at: window_start..window_end).order(:id)
end

#accounts_created_by_groupHash{Symbol => Array<Account>}

Split: WWW = customer site (Customer party), CRM = employee app (Employee party).

Returns:

  • (Hash{Symbol => Array<Account>})

    :www, :crm, and :other buckets



93
94
95
# File 'app/services/account_daily_activity_report.rb', line 93

def accounts_created_by_group
  split_by_www_crm(accounts_created)
end

#login_eventsActiveRecord::Relation<LoginActivity>

Per-event sign-in attempts (success + failure) recorded by AuthTrail
in login_activities. Replaces the previous Devise current_sign_in_at
snapshot, which collapsed multi-login days into a single row per account.

Returns:



87
88
89
# File 'app/services/account_daily_activity_report.rb', line 87

def 
  LoginActivity.where(created_at: window_start..window_end).order(:created_at)
end

#login_events_by_groupHash{Symbol => Hash{Symbol => Array<LoginActivity>}}

Bucket every LoginActivity row in the window by audience and outcome:

{
www: { success: [...], failure: [...] },
crm: { success: [...], failure: [...] },
other: { success: [...], failure: [...] }, # Account exists but party is neither Customer nor Employee
unmatched: { success: [...], failure: [...] }, # No Account row matched (typical for failed attempts)
}

Each event also carries a memoised :account (or nil) and :party
(or nil) on the row's instance_variable_get(:@report_*) shims so the
mailer view can render them without re-querying.

Returns:

  • (Hash{Symbol => Hash{Symbol => Array<LoginActivity>}})

    events bucketed by audience and outcome



110
111
112
113
114
# File 'app/services/account_daily_activity_report.rb', line 110

def 
  events = .to_a
  grouped = bucket_events(events, preload_accounts(events))
  ensure_all_buckets(grouped)
end