late 0.0.1055

API reference for Zernio. Authenticate with a Bearer API key. Base URL: https://zernio.com/api Versioning and deprecation: all endpoints are versioned in the URL path (current version: /v1). Breaking changes only ship in a new path version; existing versions keep working. Deprecated operations are marked 'deprecated: true' in this spec and announced in the changelog (https://zernio.com/changelog) before removal. Errors: every 4xx/5xx response is application/json with a machine-readable 'code' and a human-readable 'error' message (see the ErrorResponse schema). Request ids: responses carry an X-Request-Id header with the id we log the request under. Quote it when reporting a problem. A valid x-request-id you send is reused as that id.
Documentation
/*
 * Zernio API
 *
 * API reference for Zernio. Authenticate with a Bearer API key. Base URL: https://zernio.com/api  Versioning and deprecation: all endpoints are versioned in the URL path (current version: /v1). Breaking changes only ship in a new path version; existing versions keep working. Deprecated operations are marked 'deprecated: true' in this spec and announced in the changelog (https://zernio.com/changelog) before removal.  Errors: every 4xx/5xx response is application/json with a machine-readable 'code' and a human-readable 'error' message (see the ErrorResponse schema).  Request ids: responses carry an X-Request-Id header with the id we log the request under. Quote it when reporting a problem. A valid x-request-id you send is reused as that id.
 *
 * The version of the OpenAPI document: 1.104.0
 * Contact: support@zernio.com
 * Generated by: https://openapi-generator.tech
 */

use crate::models;
use serde::{Deserialize, Serialize};

#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct AdMetrics {
    #[serde(rename = "spend", skip_serializing_if = "Option::is_none")]
    pub spend: Option<f64>,
    #[serde(rename = "impressions", skip_serializing_if = "Option::is_none")]
    pub impressions: Option<i32>,
    /// Unique people reached in the requested date range. Meta (facebook/instagram) and TikTok: the platform's own de-duplicated reach for the exact range, fetched live and cached up to ~1 hour (may lag recent delivery; on a transient platform error the value temporarily falls back to a sum of per-day reach, which overcounts people reached on multiple days or by multiple child ads). Because it is de-duplicated, reach is NOT additive on these platforms: neither daily values nor child nodes sum to the range total. LinkedIn: the campaign's `approximateMemberReach`, de-duplicated by LinkedIn per day and then summed over the range here, so it overcounts members reached on more than one day; LinkedIn withholds it below its audience privacy threshold, where it reads 0. Google, X, Pinterest and OpenAI report 0 (reach not synced). Frequency (impressions / reach) is only meaningful for Meta and TikTok.
    #[serde(rename = "reach", skip_serializing_if = "Option::is_none")]
    pub reach: Option<i32>,
    #[serde(rename = "clicks", skip_serializing_if = "Option::is_none")]
    pub clicks: Option<i32>,
    /// Click-through rate (%)
    #[serde(rename = "ctr", skip_serializing_if = "Option::is_none")]
    pub ctr: Option<f64>,
    /// Cost per click
    #[serde(rename = "cpc", skip_serializing_if = "Option::is_none")]
    pub cpc: Option<f64>,
    /// Cost per 1000 impressions
    #[serde(rename = "cpm", skip_serializing_if = "Option::is_none")]
    pub cpm: Option<f64>,
    #[serde(rename = "engagement", skip_serializing_if = "Option::is_none")]
    pub engagement: Option<i32>,
    /// Count of conversion events over the requested date range. FRACTIONAL: attribution splits one conversion across touchpoints and Google additionally reports modeled conversions, so values like 0.347 are normal. Meta: events matching the campaign's promoted_object.custom_event_type (PURCHASE, LEAD, etc.). Google: the account's tracked conversions. X and LinkedIn: their reported website/lead conversions (added 2026-07). 0 for non-conversion campaigns or when no events have fired.
    #[serde(rename = "conversions", skip_serializing_if = "Option::is_none")]
    pub conversions: Option<f64>,
    /// All conversions, including actions excluded from the Conversions column (Google metrics.all_conversions). 0 on platforms without the concept.
    #[serde(rename = "allConversions", skip_serializing_if = "Option::is_none")]
    pub all_conversions: Option<f64>,
    /// Derived spend / conversions in the same currency as spend. 0 when conversions is 0.
    #[serde(rename = "costPerConversion", skip_serializing_if = "Option::is_none")]
    pub cost_per_conversion: Option<f64>,
    /// Per-action-type counts summed over the date range, keyed by the platform's action-type names. Meta: raw Insights action_type keys (link_click, offsite_conversion.fb_pixel_purchase, onsite_conversion.lead_grouped, ...), covering both engagement and conversion events. TikTok: pixel conversions (purchase, add_to_cart, initiate_checkout, view_content, complete_payment, lead) plus the paid-engagement family (follow, post_reaction for paid likes, comment, share). Follow is how FOLLOWERS-goal campaigns report their result. X: conversion types (purchase, sign_up, site_visit, download, custom). LinkedIn: conversion types (post_click, post_view, lead_gen) plus `video_view`, which carries LinkedIn's `videoViews` under the same key every other platform uses, so it feeds `engagementBreakdown.videoViews` too; it is an engagement, NOT a conversion, and is excluded from `conversions`. Google returns {} (its per-action names aren't synced per ad). Empty object when no actions are reported. NOTE: keys differ by platform, so branch on the ad's platform when interpreting them.
    #[serde(rename = "actions", skip_serializing_if = "Option::is_none")]
    pub actions: Option<std::collections::HashMap<String, i32>>,
    /// Monetary mirror of `actions`, from Meta's Insights `action_values[]` array. Same keying: values are the revenue attributed to each action_type, in ad-account native currency (same unit as `spend`; see the campaign node's `currency` field). Use this to compute revenue-per-event (e.g. avg purchase value). Meta-only; other platforms return {}.
    #[serde(rename = "actionValues", skip_serializing_if = "Option::is_none")]
    pub action_values: Option<std::collections::HashMap<String, f64>>,
    /// Convenience sum of purchase-type action values, picked from `actionValues` via the same priority list as `conversions` so both fields describe the same events. In ad-account native currency. 0 when the campaign has no purchase event configured. Meta-only.
    #[serde(rename = "purchaseValue", skip_serializing_if = "Option::is_none")]
    pub purchase_value: Option<f64>,
    /// Return on ad spend, derived as `purchaseValue / spend`. 0 when `spend` is 0. Equivalent to Meta's `purchase_roas` under default attribution. At ad-set and campaign levels this is recomputed from summed purchaseValue + spend (NOT averaged across children) so it's mathematically correct at every rollup level.
    #[serde(rename = "roas", skip_serializing_if = "Option::is_none")]
    pub roas: Option<f64>,
    /// Derived `spend / actions[type]` for every action type with a non-zero count, in ad-account native currency. Same keys as `actions`. Rounded to 4 decimals because cheap actions cost well under a cent. Recomputed from summed spend + counts at every rollup level. Empty object when spend is 0 or no actions are reported.
    #[serde(rename = "costPerAction", skip_serializing_if = "Option::is_none")]
    pub cost_per_action: Option<std::collections::HashMap<String, f64>>,
    /// Clicks leading off Meta's surfaces to the advertiser's destination. Meta-only; other platforms report 0.
    #[serde(rename = "outboundClicks", skip_serializing_if = "Option::is_none")]
    pub outbound_clicks: Option<i32>,
    /// Derived `outboundClicks / impressions * 100`, recomputed from sums at every rollup level.
    #[serde(rename = "outboundClicksCtr", skip_serializing_if = "Option::is_none")]
    pub outbound_clicks_ctr: Option<f64>,
    /// In-session link clicks. Differs from the attributed `link_click` count in `actions`/`engagementBreakdown.linkClicks`, which uses the attribution window. Meta-only.
    #[serde(rename = "inlineLinkClicks", skip_serializing_if = "Option::is_none")]
    pub inline_link_clicks: Option<i32>,
    /// Derived `inlineLinkClicks / impressions * 100`, recomputed from sums at every rollup level.
    #[serde(rename = "inlineLinkClickCtr", skip_serializing_if = "Option::is_none")]
    pub inline_link_click_ctr: Option<f64>,
    /// People who clicked at least once. NOT additive: summed across days/children it overcounts people who clicked on multiple days or ads, so treat rollups as an upper bound (same caveat as `reach`). Meta-only.
    #[serde(rename = "uniqueClicks", skip_serializing_if = "Option::is_none")]
    pub unique_clicks: Option<i32>,
    /// Derived `uniqueClicks / impressions * 100` (NOT Meta's reach-based unique_ctr). Inherits the non-additivity caveat of `uniqueClicks`.
    #[serde(rename = "uniqueCtr", skip_serializing_if = "Option::is_none")]
    pub unique_ctr: Option<f64>,
    /// Number of times the video started playing, summed over the date range and across children at ad-set/campaign level. 0 for non-video ads. Sources: Meta `video_play_actions`, TikTok `video_play_actions`.
    #[serde(rename = "videoPlayActions", skip_serializing_if = "Option::is_none")]
    pub video_play_actions: Option<i32>,
    /// Views of at least 30 seconds (or to the end, for shorter videos). Sources: Meta `video_30_sec_watched_actions` (Meta only).
    #[serde(
        rename = "video30SecWatchedActions",
        skip_serializing_if = "Option::is_none"
    )]
    pub video30_sec_watched_actions: Option<i32>,
    /// ThruPlays (watched to completion, or at least 15 seconds). Sources: Meta `video_thruplay_watched_actions` (Meta only).
    #[serde(
        rename = "videoThruplayWatchedActions",
        skip_serializing_if = "Option::is_none"
    )]
    pub video_thruplay_watched_actions: Option<i32>,
    /// Views reaching 25% of the video's length. With the other percentile fields, powers hook/hold/drop-off analysis (e.g. hook rate = videoP25WatchedActions / videoPlayActions). Sources: Meta `video_p25_watched_actions`, TikTok `video_views_p25`.
    #[serde(
        rename = "videoP25WatchedActions",
        skip_serializing_if = "Option::is_none"
    )]
    pub video_p25_watched_actions: Option<i32>,
    /// Views reaching 50% of the video's length. Sources: Meta `video_p50_watched_actions`, TikTok `video_views_p50`.
    #[serde(
        rename = "videoP50WatchedActions",
        skip_serializing_if = "Option::is_none"
    )]
    pub video_p50_watched_actions: Option<i32>,
    /// Views reaching 75% of the video's length. Sources: Meta `video_p75_watched_actions`, TikTok `video_views_p75`.
    #[serde(
        rename = "videoP75WatchedActions",
        skip_serializing_if = "Option::is_none"
    )]
    pub video_p75_watched_actions: Option<i32>,
    /// Views reaching 95% of the video's length. Sources: Meta `video_p95_watched_actions` (Meta only).
    #[serde(
        rename = "videoP95WatchedActions",
        skip_serializing_if = "Option::is_none"
    )]
    pub video_p95_watched_actions: Option<i32>,
    /// Views reaching 100% of the video's length. Sources: Meta `video_p100_watched_actions`, TikTok `video_views_p100`.
    #[serde(
        rename = "videoP100WatchedActions",
        skip_serializing_if = "Option::is_none"
    )]
    pub video_p100_watched_actions: Option<i32>,
    /// Average seconds watched per play. Aggregated over date ranges and across children as a play-weighted average (total watch time / total plays), never a plain average of averages. Sources: Meta `video_avg_time_watched_actions`, TikTok `average_video_play`.
    #[serde(
        rename = "videoAvgTimeWatchedActions",
        skip_serializing_if = "Option::is_none"
    )]
    pub video_avg_time_watched_actions: Option<f64>,
    /// Derived `spend / videoThruplayWatchedActions`, in ad-account native currency. Rounded to 4 decimals rather than the usual 2 because a ThruPlay routinely costs well under a cent. 0 when the ad has no ThruPlays (ThruPlay is Meta-only).
    #[serde(rename = "costPerThruplay", skip_serializing_if = "Option::is_none")]
    pub cost_per_thruplay: Option<f64>,
    #[serde(rename = "funnel", skip_serializing_if = "Option::is_none")]
    pub funnel: Option<Box<models::AdFunnelCounts>>,
    #[serde(
        rename = "engagementBreakdown",
        skip_serializing_if = "Option::is_none"
    )]
    pub engagement_breakdown: Option<Box<models::AdEngagementCounts>>,
    /// Present on individual ads only, not on campaign aggregations
    #[serde(rename = "lastSyncedAt", skip_serializing_if = "Option::is_none")]
    pub last_synced_at: Option<String>,
}

impl AdMetrics {
    pub fn new() -> AdMetrics {
        AdMetrics {
            spend: None,
            impressions: None,
            reach: None,
            clicks: None,
            ctr: None,
            cpc: None,
            cpm: None,
            engagement: None,
            conversions: None,
            all_conversions: None,
            cost_per_conversion: None,
            actions: None,
            action_values: None,
            purchase_value: None,
            roas: None,
            cost_per_action: None,
            outbound_clicks: None,
            outbound_clicks_ctr: None,
            inline_link_clicks: None,
            inline_link_click_ctr: None,
            unique_clicks: None,
            unique_ctr: None,
            video_play_actions: None,
            video30_sec_watched_actions: None,
            video_thruplay_watched_actions: None,
            video_p25_watched_actions: None,
            video_p50_watched_actions: None,
            video_p75_watched_actions: None,
            video_p95_watched_actions: None,
            video_p100_watched_actions: None,
            video_avg_time_watched_actions: None,
            cost_per_thruplay: None,
            funnel: None,
            engagement_breakdown: None,
            last_synced_at: None,
        }
    }
}