Class: InternalReportsMailer

Inherits:
InternalNotificationMailer show all
Defined in:
app/mailers/internal_reports_mailer.rb

Overview

Scheduled internal reports and digests (calls, leads, KPIs, daily focus).

Extracted from the former InternalMailer god object.

See Also:

  • InternalNotificationMailer
  • doc/tasks/202606131218_INTERNAL_MAILER_DECOMPOSITIONdoc/tasks/202606131218_INTERNAL_MAILER_DECOMPOSITION.md

Constant Summary collapse

DAILY_FOCUS_CC =

Daily focus cc.

%w[epasek@warmlyyours.com jbillen@warmlyyours.com mgodawa@warmlyyours.com].freeze
OUTLET_PURCHASE_DIGEST_CC =

Outlet-purchase digest (OutletPurchaseDigestWorker). Fixed list rather
than the sales_manager/accounting_rep roles that hold the verification
ability — the reviewers were named explicitly, and a role sweep would mail
people who never asked for it.

%w[
  vadepu@warmlyyours.com
  epasek@warmlyyours.com
  mgodawa@warmlyyours.com
  jbillen@warmlyyours.com
  cbillen@warmlyyours.com
  ocamuendo@warmlyyours.com
].freeze
DAILY_FOCUS_DIGEST_RECIPIENTS =

Consolidated manager digest (DailyFocusManagerReportWorker); fixed list — not all sales_manager/sales_director roles.

%w[
  epasek@warmlyyours.com
  mgodawa@warmlyyours.com
  jbillen@warmlyyours.com
  srosenbaum@warmlyyours.com
].freeze

Instance Method Summary collapse

Methods inherited from InternalNotificationMailer

#default_url_options

Methods included from SendgridSmtpApi::InternalMailerHeaders

#mail

Methods inherited from ApplicationMailer

#null_mail

Instance Method Details

#activities_reportMail::Message

Today's activities across the team.

Returns:

  • (Mail::Message)


179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'app/mailers/internal_reports_mailer.rb', line 179

def activities_report
  options = {}
  options[:date] = Date.current.to_date
  options[:employee_ids] = ['']
  options[:role_ids] = ['']
  @report_command = ::Report::ActivityPerformance::Command.new(options)
  @results = @report_command.execute

  mail(from: 'Heatwave Team <heatwaveteam@warmlyyours.com>',
       to: 'jbillen@warmlyyours.com;epasek@warmlyyours.com;srosenbaum@warmlyyours.com',
       cc: 'heatwaveteam@warmlyyours.com',
       subject: 'Activities Report')
end

#amazon_repricing_report(report_data) ⇒ Mail::Message

Sends a summary report after Amazon pricing automation runs.
Reports on price increases, price decreases, and blocked price raises.

Parameters:

  • report_data (Hash)

    containing:

    • processed_count: total items processed
    • price_increased_count: items that had price raised
    • price_lowered_count: items that had price lowered
    • flagged_count: items flagged for manual review
    • blocked_by_external: array of items blocked by external retailer prices
    • messages: array of status messages

Returns:

  • (Mail::Message)


247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'app/mailers/internal_reports_mailer.rb', line 247

def amazon_repricing_report(report_data)
  @report_data = report_data
  @processed_count = report_data[:processed_count] || 0
  @price_increased_count = report_data[:price_increased_count] || 0
  @price_lowered_count = report_data[:price_lowered_count] || 0
  @price_reverted_count = report_data[:price_reverted_count] || 0
  @flagged_count = report_data[:flagged_count] || 0
  @blocked_by_external = report_data[:blocked_by_external] || []
  @messages = report_data[:messages] || []

  # Skip email if nothing significant happened
  return null_mail if @processed_count.zero? && @blocked_by_external.empty?

  # Only send if there were price changes or blocks
  return null_mail if @price_increased_count.zero? && @price_lowered_count.zero? && @price_reverted_count.zero? && @blocked_by_external.empty?

  subject_parts = []
  subject_parts << "#{@price_increased_count} raised" if @price_increased_count.positive?
  subject_parts << "#{@price_lowered_count} lowered" if @price_lowered_count.positive?
  subject_parts << "#{@price_reverted_count} reverted" if @price_reverted_count.positive?
  subject_parts << "#{@blocked_by_external.size} blocked" if @blocked_by_external.any?

  subject = "Amazon Repricing Report: #{subject_parts.join(', ')}"

  mail(from: 'Heatwave Team <heatwaveteam@warmlyyours.com>',
       to: 'ediadmin@warmlyyours.com',
       cc: 'heatwaveteam@warmlyyours.com',
       subject: subject)
end

#auto_schema_extraction_report(report_data) ⇒ Mail::Message

Parameters:

  • report_data (Hash{Symbol=>Object})

    as described above

Returns:

  • (Mail::Message)


222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'app/mailers/internal_reports_mailer.rb', line 222

def auto_schema_extraction_report(report_data)
  @report_data = report_data
  @articles_processed = report_data[:articles_processed] || 0
  @errors = report_data[:errors] || 0
  @total_articles = report_data[:total_articles] || 0
  @message = report_data[:message] || 'No message provided'
  @processed_articles = report_data[:processed_articles] || []
  @error_articles = report_data[:error_articles] || []

  mail(from: 'Heatwave Team <heatwaveteam@warmlyyours.com>',
       to: 'Heatwave Team <heatwaveteam@warmlyyours.com>',
       subject: "Auto Schema Extraction Report - #{@articles_processed} articles processed")
end

#call_statistics_report(date = nil) ⇒ Mail::Message

Per-department call statistics for a single working day.

Parameters:

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

    the day to report on; defaults to the previous
    working day

Returns:

  • (Mail::Message)


40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'app/mailers/internal_reports_mailer.rb', line 40

def call_statistics_report(date = nil)
  date ||= 1.working.day.ago.to_time

  options = {}
  options[:period1_gteq] = date.beginning_of_day
  options[:period1_lteq] = date.end_of_day
  options[:departments] = ['Accounting', 'Business Development', 'Customer Service', 'Sales', 'Tech Support', 'Warehouse']
  options[:employee_ids] = []
  @result = Report::CallStatistics::Query.report(options)
  subject_dep = 'Inbound/Outbound calls per rep.'
  mail(from: 'Heatwave Team <heatwaveteam@warmlyyours.com>',
       to: 'call_report@warmlyyours.com',
       cc: 'heatwaveteam@warmlyyours.com',
       subject: subject_dep)
end

#campaign_summary(campaign_emails) ⇒ Mail::Message

Monthly campaign-manager summary. Sends nothing when the month had no
campaign emails.

Parameters:

Returns:

  • (Mail::Message)

    a null mail when there is nothing to report



210
211
212
213
214
215
216
217
218
# File 'app/mailers/internal_reports_mailer.rb', line 210

def campaign_summary(campaign_emails)
  @campaign_emails = campaign_emails
  return null_mail if @campaign_emails.blank?

  mail(from: ADMINISTRATOR_EMAIL,
       to: "#{MARKETING_TEAM_EMAIL}, epasek@warmlyyours.com",
       cc: 'heatwaveteam@warmlyyours.com',
       subject: 'Heatwave Campaign Manager Monthly Summary')
end

#daily_account_activity_reportMail::Message

Yesterday's account activity, from AccountDailyActivityReport.

Returns:

  • (Mail::Message)


114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'app/mailers/internal_reports_mailer.rb', line 114

def 
  report = AccountDailyActivityReport.for_scheduled_run
  @report_date = report.date
  @report_rolling_hours = report.rolling_hours
  @window_start = report.window_start
  @window_end = report.window_end
  created = report.accounts_created_by_group
  @accounts_created_www = created[:www]
  @accounts_created_crm = created[:crm]
  @accounts_created_other = created[:other]
  @login_events_by_group = report.

  subject = if @report_rolling_hours
              "Account activity — last #{@report_rolling_hours}h (America/Chicago)"
            else
              "Daily account activity — #{@report_date.strftime('%Y-%m-%d')} (America/Chicago)"
            end

  mail(from: 'Heatwave Team <heatwaveteam@warmlyyours.com>',
       to: 'sysadmin@warmlyyours.com',
       subject: subject)
end

#daily_call_record_reportMail::Message

Consolidated daily call-record health email — one message covering BOTH the
prior day's import/transcription pipeline health (Report::DailyImportSummary)
and the recording-gap reconciliation (Report::CallRecordingGaps). Replaces
the two former mailers (daily_import_summary + call_recording_gaps_report)
that fired separately at 7am and 8am. The gaps CSV is attached when there are
gaps. The subject flags ⚠ when either half is unhealthy (import issues OR
genuinely missing audio). Sent by DailyCallRecordReportWorker, which refreshes
the call-log↔recording links right before this runs.

Returns:

  • (Mail::Message)


358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# File 'app/mailers/internal_reports_mailer.rb', line 358

def daily_call_record_report
  @import = Report::DailyImportSummary.for_scheduled_run
  @gaps   = Report::CallRecordingGaps.for_scheduled_run

  if @gaps.gap_count.positive?
    attachments["call-recording-gaps-#{@gaps.date.strftime('%Y-%m-%d')}.csv"] =
      { mime_type: 'text/csv', content: @gaps.to_csv }
  end

  all_clear = @import.all_clear? && @gaps.absent_count.zero?
  subject = "#{all_clear ? '' : ''} Call records #{@import.date.strftime('%Y-%m-%d')}" \
            "#{@import.imported_calls} imported, #{@import.transcribed_ok} transcribed; " \
            "#{@gaps.absent_count} missing audio, #{@gaps.unlinked_count} unlinked " \
            "of #{@gaps.expected} (#{@gaps.coverage_rate}% linked)"

  mail(from: 'Heatwave Team <heatwaveteam@warmlyyours.com>',
       to: 'heatwaveteam@warmlyyours.com',
       subject: subject)
end

#daily_focus_approved(employee:, conversation:) ⇒ Mail::Message

One rep's approved Daily Focus, sent to that rep.

Parameters:

Returns:

  • (Mail::Message)


329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
# File 'app/mailers/internal_reports_mailer.rb', line 329

def daily_focus_approved(employee:, conversation:)
  @employee = employee
  @conversation = conversation

  raw_content = conversation.assistant_messages
                            .where(role: 'assistant')
                            .order(:id)
                            .last
                            &.content
  @analysis_html = (Assistant::ResponseFormatter.new(raw_content).format if raw_content.present?)

  mail(
    from: 'Heatwave Team <heatwaveteam@warmlyyours.com>',
    to: @employee.email,
    cc: DAILY_FOCUS_CC,
    subject: "Your Daily Focus for #{Date.current.strftime('%A, %B %-d')} is ready"
  )
end

#daily_focus_digest(conversations:, recipient_emails:) ⇒ Mail::Message

One digest covering every rep's Daily Focus conversation for today.

Parameters:

Returns:

  • (Mail::Message)


313
314
315
316
317
318
319
320
321
322
# File 'app/mailers/internal_reports_mailer.rb', line 313

def daily_focus_digest(conversations:, recipient_emails:)
  @conversations = conversations
  @date = Date.current
  @crm_host = CRM_HOSTNAME

  mail(
    to: recipient_emails,
    subject: "Daily Focus Digest — #{@date.strftime('%A, %B %-d, %Y')} (#{conversations.size} reps)"
  )
end

#dealer_locator_communication_report(locator_records, activity_task) ⇒ Mail::Message

Locator records attached to one activity task, for the rep working it.

Parameters:

  • locator_records (Enumerable<LocatorRecord>)
  • activity_task (String)

    names the task, and appears in the subject

Returns:

  • (Mail::Message)


167
168
169
170
171
172
173
174
# File 'app/mailers/internal_reports_mailer.rb', line 167

def dealer_locator_communication_report(locator_records, activity_task)
  @locator_records = locator_records
  @activity_task = activity_task
  mail(from: 'Heatwave Team <heatwaveteam@warmlyyours.com>',
       to: 'epasek@warmlyyours.com',
       cc: 'heatwaveteam@warmlyyours.com',
       subject: "Locator Record: #{activity_task}")
end

#kpi_call_report(department, recipient) ⇒ Mail::Message

Weekly KPI call figures for one department, addressed to one recipient.

Parameters:

  • department (String)

    department name, used in the subject and the query

  • recipient (String)

    email address to send to

Returns:

  • (Mail::Message)


61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'app/mailers/internal_reports_mailer.rb', line 61

def kpi_call_report(department, recipient)
  end_date = Date.current - 3
  start_date = end_date.beginning_of_week

  options = {}
  options[:period1_gteq] = start_date
  options[:period1_lteq] = end_date
  options[:departments] = [department]
  options[:employee_ids] = []
  @result = Report::KpiCall::KpiCall.result_report(options)
  subject_dep = "KPI Call Report #{department}"
  mail(from: 'Heatwave Team <heatwaveteam@warmlyyours.com>',
       to: recipient,
       cc: 'jbillen@warmlyyours.com;epasek@warmlyyours.com;mgodawa@warmlyyours.com',
       subject: subject_dep)
end

#lead_email_reportMail::Message

Leads created three days ago, so the follow-up window has had time to run.

Returns:

  • (Mail::Message)


81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'app/mailers/internal_reports_mailer.rb', line 81

def lead_email_report
  date = Date.current - 3
  sql = <<-SQL.squish
    select
    timezone('America/Chicago', a.created_at)::date,
    c.id as customer_id,
    'CN' || c.id::varchar as customer_number,
    c.full_name as customer_name,
    c.state as customer_state,
    s.full_name as visit_source_name,
    at.task_type as lead_activity_type,
    v.id as visit_id,
    a.id as activity_id
    from activities a
    inner join visits v on v.id = a.visit_id
    left join sources s on s.id = v.source_id
    inner join activity_types at on at.id = a.activity_type_id
    inner join parties c on c.id = a.customer_id
    where at.task_type LIKE 'LEAD%'
    and timezone('America/Chicago', a.created_at)::date = '#{date}'
    order by a.created_at desc
  SQL

  @result = ActiveRecord::Base.lease_connection.execute(sql).to_a.map(&:symbolize_keys)

  mail(from: 'Heatwave Team <heatwaveteam@warmlyyours.com>',
       to: 'jbillen@warmlyyours.com;',
       subject: 'Leads Created Yesterday')
end

#llm_model_sync_report(report_data) ⇒ Mail::Message

Weekly LLM model sync report.
Sent after WeeklyLlmModelSyncWorker runs (only when something changed).
Includes a stale-defaults action item when LlmDefaults constants are behind.

Parameters:

  • report_data (Hash)

    :models_total [Integer] total models in llm_models after sync
    :models_added [Integer] net new models added this run
    :synced_at [Time] when the sync completed
    :stale_defaults [Hash] output of LlmDefaults.stale_defaults (empty = all good)

Returns:

  • (Mail::Message)


288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'app/mailers/internal_reports_mailer.rb', line 288

def llm_model_sync_report(report_data)
  @models_total       = report_data[:models_total]
  @models_added       = report_data[:models_added]
  @new_models         = report_data[:new_models] || []
  @synced_at          = report_data[:synced_at]
  @stale_defaults     = report_data[:stale_defaults] || {}
  @unavailable_models = report_data[:unavailable_models] || {}

  issue_count = @stale_defaults.size + @unavailable_models.size
  subject = if issue_count > 0
              "⚠️ LLM Model Sync — #{issue_count} issue(s) need attention"
            else
              "✅ LLM Model Sync — #{@models_total} models, all constants current"
            end

  mail(from: 'Heatwave Team <heatwaveteam@warmlyyours.com>',
       to:   'Heatwave Team <heatwaveteam@warmlyyours.com>',
       subject: subject)
end

#outlet_purchase_digest(since: 1.day.ago) ⇒ Mail::Message

Yesterday's outlet-purchase matches: customers who bought through a retailer
instead of ordering direct.

Sent even when empty is NOT the intent — the worker skips a silent day, so a
digest arriving means there is something to look at.

Parameters:

  • since (ActiveSupport::TimeWithZone) (defaults to: 1.day.ago)

    window start

Returns:

  • (Mail::Message)


386
387
388
389
390
391
392
393
394
395
396
397
398
399
# File 'app/mailers/internal_reports_mailer.rb', line 386

def outlet_purchase_digest(since: 1.day.ago)
  @since = since
  query = CustomerOutletPurchaseQuery.new.created_since(since)
  @purchases = query.for_display
  @total_revenue = query.total_revenue
  @awaiting_verification = CustomerOutletPurchaseQuery.new.awaiting_verification.relation.count
  @accepted = CustomerOutletPurchaseQuery.new.accepted.relation.count

  mail(from: 'Orders <orders@warmlyyours.com>',
       to: 'orders@warmlyyours.com',
       cc: OUTLET_PURCHASE_DIGEST_CC.join(', '),
       subject: "#{@purchases.size} new outlet purchase #{'match'.pluralize(@purchases.size)} " \
                "(#{ActiveSupport::NumberHelper.number_to_currency(@total_revenue)})")
end

#report_mailer(report_command) ⇒ Mail::Message

Generic envelope for a report object that knows its own recipients and
subject — the command supplies email_parameters.

Parameters:

  • report_command (#execute, #email_parameters)

Returns:

  • (Mail::Message)


198
199
200
201
202
203
# File 'app/mailers/internal_reports_mailer.rb', line 198

def report_mailer(report_command)
  report_command.execute
  @report_command = report_command
  @report_partial = report_command.email_partial
  mail(report_command.email_parameters)
end

#retailer_compliance_report(report: nil) ⇒ Mail::Message

Daily per-retailer compliance digest (MAP violations, listing issues, scrape
failures, out-of-stock, promotions, etc.) for the externally price-checked
catalogs plus the Amazon Seller catalogs. Scheduled ~6 AM CT, after
retailer_probe_worker and listing_issue_sync_worker (see
config/sidekiq_production_schedule.yml).

Parameters:

Returns:

  • (Mail::Message)


146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'app/mailers/internal_reports_mailer.rb', line 146

def retailer_compliance_report(report: nil)
  @report = report || Retailer::DailyComplianceReport.for_scheduled_run
  @generated_at = @report.generated_at
  @rows = @report.rows
  @rows_by_region = @report.rows_by_region
  @totals = @report.totals
  @flagged_rows = @report.flagged_rows

  subject = "Retailer compliance — #{@totals[:map_violations]} MAP violations, " \
            "#{@totals[:listing_issues]} listing issues (#{@generated_at.strftime('%Y-%m-%d')})"

  mail(from: 'Heatwave Team <heatwaveteam@warmlyyours.com>',
       to: 'cbillen@warmlyyours.com',
       subject: subject)
end