access_token = trim( (string) $access_token ); $this -> api_version = trim( (string) $api_version ) ?: 'v25.0'; } public function is_configured() { return $this -> access_token !== ''; } public function get_api_version() { return $this -> api_version; } public static function get_setting( $key ) { global $mdb; return $mdb -> get( 'settings', 'setting_value', [ 'setting_key' => $key ] ); } public static function set_setting( $key, $value ) { global $mdb; if ( $mdb -> count( 'settings', [ 'setting_key' => $key ] ) ) { $mdb -> update( 'settings', [ 'setting_value' => $value ], [ 'setting_key' => $key ] ); } else { $mdb -> insert( 'settings', [ 'setting_key' => $key, 'setting_value' => $value ] ); } if ( $key === 'facebook_ads_last_error' ) { $error_at = null; if ( $value !== null && trim( (string) $value ) !== '' ) { $error_at = date( 'Y-m-d H:i:s' ); } if ( $mdb -> count( 'settings', [ 'setting_key' => 'facebook_ads_last_error_at' ] ) ) { $mdb -> update( 'settings', [ 'setting_value' => $error_at ], [ 'setting_key' => 'facebook_ads_last_error_at' ] ); } else { $mdb -> insert( 'settings', [ 'setting_key' => 'facebook_ads_last_error_at', 'setting_value' => $error_at ] ); } } } public static function normalize_ad_account_id( $account_id ) { $account_id = trim( (string) $account_id ); if ( $account_id === '' ) { return null; } if ( stripos( $account_id, 'act_' ) === 0 ) { $digits = preg_replace( '/\D+/', '', substr( $account_id, 4 ) ); return $digits !== '' ? 'act_' . $digits : null; } $digits = preg_replace( '/\D+/', '', $account_id ); if ( $digits === '' ) { return null; } return 'act_' . $digits; } public function fetch_active_insights_last_days( $account_id, $days = 30, $active_only = true ) { $days = max( 1, min( 90, (int) $days ) ); $since = date( 'Y-m-d', strtotime( '-' . ( $days - 1 ) . ' days' ) ); $until = date( 'Y-m-d' ); return $this -> fetch_active_insights_range( $account_id, $since, $until, $active_only, $days ); } public function fetch_active_insights_for_date( $account_id, $date, $active_only = true ) { $timestamp = strtotime( (string) $date ); if ( !$timestamp ) { self::set_setting( 'facebook_ads_last_error', 'Niepoprawna data dla synchronizacji Facebook Ads.' ); return false; } $sync_date = date( 'Y-m-d', $timestamp ); return $this -> fetch_active_insights_range( $account_id, $sync_date, $sync_date, $active_only, 1 ); } public function fetch_active_insights_for_range( $account_id, $since, $until, $active_only = true ) { $since_ts = strtotime( (string) $since ); $until_ts = strtotime( (string) $until ); if ( !$since_ts || !$until_ts ) { self::set_setting( 'facebook_ads_last_error', 'Niepoprawny zakres dat dla synchronizacji Facebook Ads.' ); return false; } $days = (int) floor( ( $until_ts - $since_ts ) / 86400 ) + 1; return $this -> fetch_active_insights_range( $account_id, date( 'Y-m-d', $since_ts ), date( 'Y-m-d', $until_ts ), $active_only, $days, true ); } public function fetch_campaigns_all_time( $account_id, $active_only = true ) { $account_id = self::normalize_ad_account_id( $account_id ); if ( !$account_id ) { return false; } if ( !$this -> is_configured() ) { return false; } $base_url = 'https://graph.facebook.com/' . rawurlencode( $this -> api_version ) . '/' . rawurlencode( $account_id ) . '/insights'; $params = [ 'access_token' => $this -> access_token, 'level' => 'campaign', 'fields' => 'campaign_id,spend,action_values,purchase_roas', 'date_preset' => 'maximum', 'limit' => 500 ]; if ( $active_only ) { $params['filtering'] = json_encode( [ [ 'field' => 'campaign.effective_status', 'operator' => 'IN', 'value' => [ 'ACTIVE' ] ] ] ); } $rows = $this -> fetch_all_pages( $base_url, $params ); if ( $rows === false ) { return false; } $purchase_action_types = [ 'purchase', 'omni_purchase', 'offsite_conversion.fb_pixel_purchase', 'web_in_store_purchase', 'onsite_conversion.purchase', 'app_custom_event.fb_mobile_purchase' ]; $campaigns = []; foreach ( $rows as $row ) { $campaign_id = (string) ( $row['campaign_id'] ?? '' ); if ( $campaign_id === '' ) { continue; } $spend = (float) ( $row['spend'] ?? 0 ); // 1. ROAS z purchase_roas (preferowane) $roas = 0.0; if ( isset( $row['purchase_roas'] ) && is_array( $row['purchase_roas'] ) ) { foreach ( $row['purchase_roas'] as $pr ) { $at = trim( (string) ( $pr['action_type'] ?? '' ) ); if ( in_array( $at, $purchase_action_types, true ) ) { $roas = round( (float) ( $pr['value'] ?? 0 ) * 100, 6 ); break; } } } // 2. Fallback: oblicz z action_values / spend $conversion_value = 0.0; if ( isset( $row['action_values'] ) && is_array( $row['action_values'] ) ) { foreach ( $row['action_values'] as $action ) { $at = trim( (string) ( $action['action_type'] ?? '' ) ); if ( in_array( $at, $purchase_action_types, true ) ) { $conversion_value += (float) ( $action['value'] ?? 0 ); break; } } } if ( $roas <= 0 && $spend > 0 && $conversion_value > 0 ) { $roas = round( ( $conversion_value / $spend ) * 100, 6 ); } $campaigns[ $campaign_id ] = [ 'campaign_id' => $campaign_id, 'spend_all_time' => $spend, 'conversion_value_all_time' => $conversion_value, 'roas_all_time' => $roas ]; } return $campaigns; } private function fetch_active_insights_range( $account_id, $since, $until, $active_only = true, $days = 0, $aggregate = false ) { $account_id = self::normalize_ad_account_id( $account_id ); if ( !$account_id ) { self::set_setting( 'facebook_ads_last_error', 'Niepoprawne Facebook Ads Account ID.' ); return false; } if ( !$this -> is_configured() ) { self::set_setting( 'facebook_ads_last_error', 'Brak tokena Facebook Ads API.' ); return false; } $since_ts = strtotime( (string) $since ); $until_ts = strtotime( (string) $until ); if ( !$since_ts || !$until_ts || $since_ts > $until_ts ) { self::set_setting( 'facebook_ads_last_error', 'Niepoprawny zakres dat Facebook Ads.' ); return false; } $since = date( 'Y-m-d', $since_ts ); $until = date( 'Y-m-d', $until_ts ); if ( (int) $days <= 0 ) { $days = (int) floor( ( $until_ts - $since_ts ) / 86400 ) + 1; } $days = max( 1, min( 90, (int) $days ) ); $base_url = 'https://graph.facebook.com/' . rawurlencode( $this -> api_version ) . '/' . rawurlencode( $account_id ) . '/insights'; $levels = [ 'campaign' => [ 'fields' => 'account_id,campaign_id,campaign_name,spend,impressions,clicks,ctr,cpc,action_values,purchase_roas,date_start,date_stop', 'filtering_field' => 'campaign.effective_status' ], 'adset' => [ 'fields' => 'account_id,campaign_id,campaign_name,adset_id,adset_name,spend,impressions,clicks,ctr,cpc,action_values,purchase_roas,date_start,date_stop', 'filtering_field' => 'adset.effective_status' ], 'ad' => [ 'fields' => 'account_id,campaign_id,campaign_name,adset_id,adset_name,ad_id,ad_name,spend,impressions,clicks,ctr,cpc,action_values,purchase_roas,date_start,date_stop', 'filtering_field' => 'ad.effective_status' ] ]; $result = [ 'meta' => [ 'account_id' => $account_id, 'api_version' => $this -> api_version, 'days' => $days, 'since' => $since, 'until' => $until, 'generated_at' => date( 'c' ) ], 'campaign' => [], 'adset' => [], 'ad' => [] ]; foreach ( $levels as $level => $cfg ) { $params = [ 'access_token' => $this -> access_token, 'level' => $level, 'fields' => $cfg['fields'], 'time_range' => json_encode( [ 'since' => $since, 'until' => $until ] ), 'limit' => 500 ]; if ( !$aggregate ) { $params['time_increment'] = 1; } if ( $active_only ) { $params['filtering'] = json_encode( [ [ 'field' => $cfg['filtering_field'], 'operator' => 'IN', 'value' => [ 'ACTIVE' ] ] ] ); } $rows = $this -> fetch_all_pages( $base_url, $params ); if ( $rows === false ) { return false; } $result[ $level ] = $rows; } self::set_setting( 'facebook_ads_last_error', null ); return $result; } private function fetch_all_pages( $url, $params = null ) { $all_rows = []; $next_url = $url; $next_params = $params; while ( $next_url ) { $payload = $this -> request_json( $next_url, $next_params ); if ( $payload === false ) { return false; } if ( isset( $payload['data'] ) && is_array( $payload['data'] ) ) { foreach ( $payload['data'] as $row ) { $all_rows[] = $row; } } $next_url = ''; $next_params = null; if ( isset( $payload['paging']['next'] ) && is_string( $payload['paging']['next'] ) ) { $next_url = $payload['paging']['next']; } } return $all_rows; } private function request_json( $url, $params = null ) { if ( is_array( $params ) ) { $query = http_build_query( $params ); $url .= ( strpos( $url, '?' ) === false ? '?' : '&' ) . $query; } $ch = curl_init( $url ); curl_setopt_array( $ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_CONNECTTIMEOUT => 20, CURLOPT_TIMEOUT => 120 ] ); $response = curl_exec( $ch ); $http_code = (int) curl_getinfo( $ch, CURLINFO_HTTP_CODE ); $curl_error = curl_error( $ch ); curl_close( $ch ); if ( $response === false ) { self::set_setting( 'facebook_ads_last_error', 'cURL error: ' . $curl_error ); return false; } $decoded = json_decode( (string) $response, true ); if ( !is_array( $decoded ) ) { self::set_setting( 'facebook_ads_last_error', 'Niepoprawny JSON odpowiedzi Meta API. HTTP ' . $http_code ); return false; } if ( isset( $decoded['error'] ) ) { $message = (string) ( $decoded['error']['message'] ?? 'Nieznany blad Meta API' ); $code = (string) ( $decoded['error']['code'] ?? '' ); $subcode = (string) ( $decoded['error']['error_subcode'] ?? '' ); self::set_setting( 'facebook_ads_last_error', 'Meta API: ' . $message . ' (code: ' . $code . ', subcode: ' . $subcode . ')' ); return false; } if ( $http_code >= 400 ) { self::set_setting( 'facebook_ads_last_error', 'Meta API HTTP ' . $http_code . ': ' . substr( (string) $response, 0, 1000 ) ); return false; } return $decoded; } }