Class: Marketing::AdSpend::AmazonAdapter
- Inherits:
-
Object
- Object
- Marketing::AdSpend::AmazonAdapter
- Defined in:
- app/services/marketing/ad_spend/amazon_adapter.rb
Overview
Amazon Ads report request and single-sweep polling primitives. Reporting API
v3 is asynchronous, so this adapter deliberately does not own a loop or a
sleep. Workers persist the returned report state in Sidekiq, poll once, and
reschedule while Amazon generates the report.
Defined Under Namespace
Classes: ReportsThrottledError
Constant Summary collapse
- PROVIDER =
Adapter key used by AdSpendSyncWorker::ADAPTERS and the recorder's provider column.
'amazon_ads'- PROFILE_TYPES =
Advertising account types we pull spend for; vendor profiles bill elsewhere.
%w[seller].freeze
- PROFILE_COUNTRIES =
Marketplaces we advertise in. Anything else is filtered out before reporting.
%w[US CA].freeze
- REPORT_RETENTION =
Amazon Ads v3 keeps report data for 60 days. Asking for anything older is a
permanent HTTP 400 ("startDate (…) must be equal to or after report type data
retention start date (…)"), so a backfill reaching past the window burned
three worker retries, failed the day's batch, left the day unmarked, and got
handed back to the self-heal run forever (AppSignal #6481, #6479). 60.days
Class Method Summary collapse
-
.poll_report(report, default_date:) ⇒ Hash
Perform one status request for one report.
-
.request_reports(start_date, end_date = start_date, reports: []) ⇒ Array<Hash>
Request one report per seller profile and ad product.
-
.retention_start_date ⇒ Date
ponytail: a request that straddles the boundary can still 400 and re-enter that loop for one day.
Class Method Details
.poll_report(report, default_date:) ⇒ Hash
Perform one status request for one report. A completed result includes
normalized campaign rows grouped by their report date; pending and failed
results carry no rows and let the worker choose reschedule/failure policy.
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
# File 'app/services/marketing/ad_spend/amazon_adapter.rb', line 108 def self.poll_report(report, default_date:) report = report.with_indifferent_access client = AmazonAds::ApiClient.new status = client.report_status(profile_id: report.fetch(:profile_id), report_id: report.fetch(:report_id)) case status['status'] when 'COMPLETED' ad_product = report.fetch(:ad_product) sales_key = AmazonAds::ApiClient::CONVERSION_COLUMNS.fetch(ad_product).fetch(:sales) rate = report.fetch(:rate).to_f rows = client.download_report(status.fetch('url')).map do |row| # Sales is money in the profile's own currency, exactly like cost, so it # takes the same multiplier. Without this a CA profile's sales would be # summed as USD and every Canadian ROAS would read ~1.4x too high. row.merge('cost' => row['cost'].to_f * rate, sales_key => row[sales_key].to_f * rate) end { state: :completed, rows_by_date: rows.group_by { |row| row['date'].present? ? Date.parse(row['date']) : default_date.to_date } .transform_values { normalize(it, ad_product:) } } when 'FAILED' { state: :failed, rows_by_date: {} } else { state: :pending, rows_by_date: {} } end end |
.request_reports(start_date, end_date = start_date, reports: []) ⇒ Array<Hash>
Request one report per seller profile and ad product. The returned hashes
are JSON-safe job state: report identity plus the FX multiplier needed
when that report eventually downloads.
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'app/services/marketing/ad_spend/amazon_adapter.rb', line 54 def self.request_reports(start_date, end_date = start_date, reports: []) reports = reports.map { it.to_h.stringify_keys } # Hoisted: retention_start_date reads Date.current, and a job crossing midnight # would otherwise compare against one floor and report another. retention_start = retention_start_date if start_date < retention_start Rails.logger.warn( "[AmazonAdapter] #{start_date} predates Amazon Ads report retention " \ "(oldest available: #{retention_start}); skipping — the data is gone, not late" ) # Hand back whatever a throttled earlier pass already created rather than []. # Those reports cost quota and are still downloadable; dropping them here # would orphan them when the retention floor advances mid-retry. return reports end existing = reports.index_by { [it.fetch('profile_id').to_s, it.fetch('ad_product')] } client = AmazonAds::ApiClient.new profiles = client.profiles.select do |profile| PROFILE_TYPES.include?(profile.dig('accountInfo', 'type')) && PROFILE_COUNTRIES.include?(profile['countryCode']) end profiles.each do |profile| missing_products = AmazonAds::ApiClient::REPORT_PRODUCTS.keys.reject do |ad_product| existing.key?([profile['profileId'].to_s, ad_product]) end next if missing_products.empty? rate = consolidation_rate(profile['currencyCode'], start_date) missing_products.each do |ad_product| report = { 'profile_id' => profile['profileId'], 'ad_product' => ad_product, 'rate' => rate, 'report_id' => client.create_report( profile_id: profile['profileId'], ad_product:, date: start_date, end_date: ) } reports << report existing[[profile['profileId'].to_s, ad_product]] = report end end reports rescue AmazonAds::ApiClient::ThrottledError => e raise ReportsThrottledError.new(e, reports:), cause: e end |
.retention_start_date ⇒ Date
ponytail: a request that straddles the boundary can still 400 and re-enter
that loop for one day. Parse the floor out of the 400 body if it ever matters.
26 27 28 |
# File 'app/services/marketing/ad_spend/amazon_adapter.rb', line 26 def self.retention_start_date Date.current - REPORT_RETENTION end |