Class: Marketing::Ads::ActionItemSync
- Inherits:
-
Object
- Object
- Marketing::Ads::ActionItemSync
- Defined in:
- app/services/marketing/ads/action_item_sync.rb
Overview
Reconciles AdActionItem rows against what the numbers currently say.
Rule-based, not LLM: each rule is a threshold over one campaign's trailing
window of SourceDataPoint metrics and its attributed invoice revenue.
LLM-generated recommendations (budget reallocation, negative keywords,
creative) are a later layer on the same table.
Reconciliation follows ListingIssues::Sync: upsert what fires now on the
(source, category, fingerprint) key, and retire what no longer fires. It
differs in one deliberate way — retirement marks items stale, not
completed, because a rule going quiet means the numbers moved, not that
anyone fixed anything. A human's completed / ignored is never
overwritten: the window is trailing, so an item resolved today keeps
matching its own rule for another 28 days, and reopening it would make the
list impossible to clear.
Idempotent — re-running the same day refreshes rows rather than stacking
duplicates.
Defined Under Namespace
Constant Summary collapse
- WINDOW_DAYS =
Trailing window the rules measure over. Reads the dashboard's own
default rather than restating it: an item's evidence has to line up with
what someone sees on /ads-overview when they go to check it, and a
second hardcoded 28 silently stopped doing that the moment the dashboard
moved to a month. ::Report::AdsOverviewReport::DEFAULT_WINDOW_DAYS
- MIN_SPEND =
Below this, a campaign isn't spending enough for any finding to be worth
someone's afternoon. 100.0- MIN_ROAS =
Return-on-ad-spend under this, with material spend, is a finding.
2.0- SPEND_ANOMALY_PCT =
Spend moving by at least this fraction versus the prior window.
0.5- SPEND_ANOMALY_MIN =
Both windows need this much spend before a swing between them means
anything — 10 → 30 dollars is a 200% "anomaly" and pure noise. 250.0- LOW_DELIVERY_MIN_IMPRESSIONS =
Impressions served with zero clicks: delivering, nobody biting.
1_000- TRACKING_MISMATCH_MIN_CONVERSIONS =
Provider-reported PURCHASE conversions we'd expect to see sales behind.
5- PURCHASE_CONVERSION_PROVIDERS =
Providers whose normalized
ad_conversionscount is specifically a
purchase count. Google and Microsoft report mixed goals (lead, quote,
purchase, etc.); OpenAI does not expose the metric. Amazon's report is
the only current adapter that maps its purchases column here. %w[amazon_ads].freeze
- HIGH_IMPACT_SPEND =
Spend at or above this makes a finding
highimpact rather thanmedium. 1_000.0- STALE_NOTE =
Note stamped on items retired because their rule stopped firing.
'auto: rule no longer fires for the current window'- ACTIONABLE_STATES =
Campaign states worth asking someone to act on. A paused campaign has
already had the action taken — surfacing it asks for work that is
done, and the item goes stale on the next pass, which is the right
answer for "the state moved." %w[ENABLED].freeze
- IMPRESSION_PRICED_COST_TYPES =
Buying models priced on impressions rather than clicks.
low_delivery
reads zero clicks as a delivery failure, which for vCPM is the model
working as bought. Only Sponsored Display carries a cost type; the rest
are click-priced and report nil. %w[vcpm].freeze
Class Method Summary collapse
Instance Method Summary collapse
- #call ⇒ Marketing::Ads::ActionItemSync::Result
-
#initialize(logger: Rails.logger, today: Date.current) ⇒ ActionItemSync
constructor
A new instance of ActionItemSync.
Constructor Details
#initialize(logger: Rails.logger, today: Date.current) ⇒ ActionItemSync
Returns a new instance of ActionItemSync.
94 95 96 97 98 99 100 |
# File 'app/services/marketing/ads/action_item_sync.rb', line 94 def initialize(logger: Rails.logger, today: Date.current) @logger = logger # The window ends yesterday: providers finalize a day overnight, so # including today would make every campaign look like it collapsed. @current_range = (today - WINDOW_DAYS)..(today - 1) @prior_range = (today - (WINDOW_DAYS * 2))..(today - WINDOW_DAYS - 1) end |
Class Method Details
.run(logger: Rails.logger, today: Date.current) ⇒ Marketing::Ads::ActionItemSync::Result
88 89 90 |
# File 'app/services/marketing/ads/action_item_sync.rb', line 88 def self.run(logger: Rails.logger, today: Date.current) new(logger:, today:).call end |
Instance Method Details
#call ⇒ Marketing::Ads::ActionItemSync::Result
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
# File 'app/services/marketing/ads/action_item_sync.rb', line 103 def call created = updated = staled = 0 stats = campaign_stats source_ids = candidate_source_ids(stats) existing = AdActionItem.where(source_id: source_ids).group_by(&:source_id) Source.where(id: source_ids).find_each do |source| c, u, s = reconcile(source, findings_for(stats[source.id]), existing[source.id] || []) created += c updated += u staled += s end Result.new(created:, updated:, staled:, campaigns: stats.size).tap do |result| @logger.info "[Marketing::Ads::ActionItemSync] #{result.to_h}" end end |