Class: Marketing::AdSpend::Recorder

Inherits:
Object
  • Object
show all
Defined in:
app/services/marketing/ad_spend/recorder.rb

Overview

Idempotently records normalized provider rows against campaign Sources.
Kept separate from the polling worker so report state transitions do not
also own Source lookup and data-point persistence policy.

Constant Summary collapse

METRICS =

Adapter row key → source_metric_type enum value. Adapters that don't
supply a key return nil for it, and SourceDataPoint.bulk_record!
compacts those away rather than writing a zero.

sales is the platform's OWN attributed revenue. Only the Amazon adapter
fills it, because Amazon is the only provider whose orders cannot reach a
campaign through invoices — see AmazonAdapter.
Everywhere else, return is read from acquisition, not from the platform.

{
  spend: :ad_spend,
  clicks: :ad_clicks,
  impressions: :ad_impressions,
  conversions: :ad_conversions,
  sales: :ad_sales
}.freeze
METRIC_KEYS =

Metric keys an adapter row may carry; all-zero rows are skipped.

METRICS.keys.freeze

Instance Method Summary collapse

Constructor Details

#initialize(provider:, batch_id: nil, logger: Rails.logger) ⇒ Recorder

Returns a new instance of Recorder.

Parameters:

  • provider (String)

    provider key (an adapter's PROVIDER)

  • batch_id (String, nil) (defaults to: nil)

    batch identifier stamped on data points

  • logger (Logger) (defaults to: Rails.logger)


30
31
32
33
34
# File 'app/services/marketing/ad_spend/recorder.rb', line 30

def initialize(provider:, batch_id: nil, logger: Rails.logger)
  @provider = provider
  @batch_id = batch_id
  @logger = logger
end

Instance Method Details

#call(rows_by_date:) ⇒ Hash

Returns persistence counts for logging/metrics.

Parameters:

  • rows_by_date (Hash{Date => Array<Hash>})

Returns:

  • (Hash)

    persistence counts for logging/metrics



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'app/services/marketing/ad_spend/recorder.rb', line 38

def call(rows_by_date:)
  sources = Source.where(campaign_provider: @provider)
                  .where.not(campaign_external_id: nil)
                  .pluck(:campaign_external_id, :id)
                  .to_h
  recorded = 0
  unmatched = Set.new

  rows_by_date.each do |date, rows|
    rows.each do |row|
      next if METRIC_KEYS.all? { |key| row[key].to_f.zero? }

      source_id = sources[row[:external_id]]
      unless source_id
        unmatched << row[:external_id]
        next
      end

      SourceDataPoint.bulk_record!(
        source_id:,
        date: date.to_date,
        batch_id: @batch_id,
        metrics: METRICS.to_h { |row_key, metric| [metric, row[row_key]] }
      )
      recorded += 1
    end
  end

  @logger.info(
    "[AdSpend::Recorder] #{@provider}: #{recorded} campaign-days recorded, " \
    "#{unmatched.size} unmatched"
  )
  { recorded:, unmatched: unmatched.size }
end