pdo -> prepare( 'SHOW COLUMNS FROM clients LIKE :column_name' ); if ( $stmt ) { $stmt -> bindValue( ':column_name', 'deleted', \PDO::PARAM_STR ); $stmt -> execute(); $has_deleted = $stmt -> fetch( \PDO::FETCH_ASSOC ) ? 1 : 0; } else { $has_deleted = 0; } } catch ( \Throwable $e ) { $has_deleted = 0; } return (bool) $has_deleted; } static private function sql_clients_not_deleted( $alias = '' ) { $alias = trim( (string) $alias ); $prefix = $alias !== '' ? $alias . '.' : ''; if ( self::clients_has_deleted_column() ) { return 'COALESCE(' . $prefix . 'deleted, 0) = 0'; } return '1=1'; } static public function get_clients_for_sync( $client_id = 0 ) { global $mdb; $client_id = (int) $client_id; $clients_not_deleted_sql = self::sql_clients_not_deleted(); $params = []; $where_client = ''; if ( $client_id > 0 ) { $where_client = ' AND id = :client_id'; $params[':client_id'] = $client_id; } return $mdb -> query( 'SELECT * FROM clients WHERE ' . $clients_not_deleted_sql . ' AND COALESCE(active, 0) = 1 AND TRIM(COALESCE(facebook_ads_account_id, \'\')) <> \'\'' . $where_client . ' ORDER BY id ASC', $params ) -> fetchAll( \PDO::FETCH_ASSOC ); } static public function sync_active_last_days_for_client( $client, $api, $days = 30, $active_only = true ) { $client = is_array( $client ) ? $client : []; $client_id = (int) ( $client['id'] ?? 0 ); $account_id = \services\FacebookAdsApi::normalize_ad_account_id( $client['facebook_ads_account_id'] ?? '' ); if ( $client_id <= 0 || !$account_id ) { return [ 'success' => false, 'client_id' => $client_id, 'errors' => [ 'Brak poprawnego client_id lub facebook_ads_account_id.' ] ]; } $days = max( 1, min( 90, (int) $days ) ); $payload = $api -> fetch_active_insights_last_days( $account_id, $days, $active_only ); if ( $payload === false ) { $last_error = trim( (string) \services\FacebookAdsApi::get_setting( 'facebook_ads_last_error' ) ); return [ 'success' => false, 'client_id' => $client_id, 'account_id' => $account_id, 'errors' => [ $last_error !== '' ? $last_error : 'Nie udalo sie pobrac danych z Meta API.' ] ]; } $save_result = self::save_snapshot( $client_id, $payload ); $save_result['client_id'] = $client_id; $save_result['account_id'] = $account_id; $save_result['days'] = $days; $save_result['meta'] = $payload['meta'] ?? []; return $save_result; } static public function sync_active_date_for_client( $client, $api, $sync_date, $active_only = true ) { $client = is_array( $client ) ? $client : []; $client_id = (int) ( $client['id'] ?? 0 ); $account_id = \services\FacebookAdsApi::normalize_ad_account_id( $client['facebook_ads_account_id'] ?? '' ); if ( $client_id <= 0 || !$account_id ) { return [ 'success' => false, 'client_id' => $client_id, 'errors' => [ 'Brak poprawnego client_id lub facebook_ads_account_id.' ] ]; } $sync_timestamp = strtotime( (string) $sync_date ); if ( !$sync_timestamp ) { return [ 'success' => false, 'client_id' => $client_id, 'account_id' => $account_id, 'errors' => [ 'Niepoprawna data synchronizacji Facebook Ads.' ] ]; } $sync_date = date( 'Y-m-d', $sync_timestamp ); $payload = $api -> fetch_active_insights_for_date( $account_id, $sync_date, $active_only ); if ( $payload === false ) { $last_error = trim( (string) \services\FacebookAdsApi::get_setting( 'facebook_ads_last_error' ) ); return [ 'success' => false, 'client_id' => $client_id, 'account_id' => $account_id, 'sync_date' => $sync_date, 'errors' => [ $last_error !== '' ? $last_error : 'Nie udalo sie pobrac danych z Meta API.' ] ]; } $save_result = self::save_snapshot( $client_id, $payload ); $save_result['client_id'] = $client_id; $save_result['account_id'] = $account_id; $save_result['days'] = 1; $save_result['sync_date'] = $sync_date; $save_result['meta'] = $payload['meta'] ?? []; return $save_result; } static public function sync_date_range_for_client( $client, $api, $since, $until, $active_only = true ) { $client = is_array( $client ) ? $client : []; $client_id = (int) ( $client['id'] ?? 0 ); $account_id = \services\FacebookAdsApi::normalize_ad_account_id( $client['facebook_ads_account_id'] ?? '' ); if ( $client_id <= 0 || !$account_id ) { return [ 'success' => false, 'client_id' => $client_id, 'errors' => [ 'Brak poprawnego client_id lub facebook_ads_account_id.' ] ]; } $since_ts = strtotime( (string) $since ); $until_ts = strtotime( (string) $until ); if ( !$since_ts || !$until_ts || $since_ts > $until_ts ) { return [ 'success' => false, 'client_id' => $client_id, 'account_id' => $account_id, 'errors' => [ 'Niepoprawny zakres dat synchronizacji Facebook Ads.' ] ]; } $since = date( 'Y-m-d', $since_ts ); $until = date( 'Y-m-d', $until_ts ); $days = (int) floor( ( $until_ts - $since_ts ) / 86400 ) + 1; $payload = $api -> fetch_active_insights_for_range( $account_id, $since, $until, $active_only ); if ( $payload === false ) { $last_error = trim( (string) \services\FacebookAdsApi::get_setting( 'facebook_ads_last_error' ) ); return [ 'success' => false, 'client_id' => $client_id, 'account_id' => $account_id, 'since' => $since, 'until' => $until, 'errors' => [ $last_error !== '' ? $last_error : 'Nie udalo sie pobrac danych z Meta API.' ] ]; } foreach ( [ 'campaign', 'adset', 'ad' ] as $level ) { if ( is_array( $payload[ $level ] ?? null ) ) { foreach ( $payload[ $level ] as &$row ) { $row['date_start'] = $until; } unset( $row ); } } $all_time_raw = $api -> fetch_campaigns_all_time( $account_id, $active_only ); $all_time_error = ''; if ( $all_time_raw === false ) { $all_time_error = (string) \services\FacebookAdsApi::get_setting( 'facebook_ads_last_error' ); } $all_time_map = is_array( $all_time_raw ) ? $all_time_raw : []; $save_result = self::save_snapshot( $client_id, $payload, $all_time_map ); $save_result['client_id'] = $client_id; $save_result['account_id'] = $account_id; $save_result['days'] = $days; $save_result['since'] = $since; $save_result['until'] = $until; $save_result['meta'] = $payload['meta'] ?? []; $save_result['all_time_debug'] = [ 'raw_type' => is_array( $all_time_raw ) ? 'array' : ( $all_time_raw === false ? 'false' : gettype( $all_time_raw ) ), 'campaigns_count' => count( $all_time_map ), 'error' => $all_time_error, 'data' => array_slice( $all_time_map, 0, 5, true ) ]; return $save_result; } static private function save_snapshot( $client_id, $payload, $all_time_map = [] ) { global $mdb; $stats = [ 'success' => false, 'campaigns_synced' => 0, 'campaign_history_synced' => 0, 'ad_sets_synced' => 0, 'ad_set_history_synced' => 0, 'ads_synced' => 0, 'ad_history_synced' => 0, 'errors' => [] ]; $campaign_map = []; $ad_set_map = []; $ad_map = []; $campaign_rows = is_array( $payload['campaign'] ?? null ) ? $payload['campaign'] : []; $ad_set_rows = is_array( $payload['adset'] ?? null ) ? $payload['adset'] : []; $ad_rows = is_array( $payload['ad'] ?? null ) ? $payload['ad'] : []; try { $started_transaction = false; if ( method_exists( $mdb -> pdo, 'beginTransaction' ) && !( $mdb -> pdo -> inTransaction() ) ) { $mdb -> pdo -> beginTransaction(); $started_transaction = true; } foreach ( $campaign_rows as $row ) { $campaign_db_id = self::upsert_campaign( $client_id, $row ); if ( $campaign_db_id <= 0 ) { continue; } $campaign_external_id = self::normalize_external_id( $row['campaign_id'] ?? '' ); if ( $campaign_external_id !== null ) { $campaign_map[ $campaign_external_id ] = $campaign_db_id; } $roas_at = (float) ( $all_time_map[ $campaign_external_id ]['roas_all_time'] ?? 0 ); if ( self::upsert_campaign_history( $campaign_db_id, $row, $roas_at ) ) { $stats['campaign_history_synced']++; } } foreach ( $ad_set_rows as $row ) { $campaign_db_id = self::resolve_campaign_db_id( $client_id, $row, $campaign_map ); $ad_set_db_id = self::upsert_ad_set( $client_id, $campaign_db_id, $row ); if ( $ad_set_db_id <= 0 ) { continue; } $ad_set_external_id = self::normalize_external_id( $row['adset_id'] ?? '' ); if ( $ad_set_external_id !== null ) { $ad_set_map[ $ad_set_external_id ] = $ad_set_db_id; } if ( self::upsert_ad_set_history( $ad_set_db_id, $row ) ) { $stats['ad_set_history_synced']++; } } foreach ( $ad_rows as $row ) { $campaign_db_id = self::resolve_campaign_db_id( $client_id, $row, $campaign_map ); $ad_set_db_id = self::resolve_ad_set_db_id( $client_id, $campaign_db_id, $row, $ad_set_map ); $ad_db_id = self::upsert_ad( $client_id, $campaign_db_id, $ad_set_db_id, $row ); if ( $ad_db_id <= 0 ) { continue; } $ad_external_id = self::normalize_external_id( $row['ad_id'] ?? '' ); if ( $ad_external_id !== null ) { $ad_map[ $ad_external_id ] = $ad_db_id; } if ( self::upsert_ad_history( $ad_db_id, $row ) ) { $stats['ad_history_synced']++; } } $stats['campaigns_synced'] = count( $campaign_map ); $stats['ad_sets_synced'] = count( $ad_set_map ); $stats['ads_synced'] = count( $ad_map ); if ( !empty( $started_transaction ) && method_exists( $mdb -> pdo, 'commit' ) && $mdb -> pdo -> inTransaction() ) { $mdb -> pdo -> commit(); } $stats['success'] = true; } catch ( \Throwable $e ) { if ( !empty( $started_transaction ) && method_exists( $mdb -> pdo, 'rollBack' ) && $mdb -> pdo -> inTransaction() ) { $mdb -> pdo -> rollBack(); } $stats['errors'][] = $e -> getMessage(); $stats['success'] = false; } return $stats; } static private function upsert_campaign( $client_id, $row ) { global $mdb; $campaign_external_id = self::normalize_external_id( $row['campaign_id'] ?? '' ); if ( $campaign_external_id === null ) { return 0; } $account_id = self::normalize_external_id( $row['account_id'] ?? '' ) ?: '0'; $campaign_name = trim( (string) ( $row['campaign_name'] ?? '' ) ); $date_sync = date( 'Y-m-d H:i:s' ); $mdb -> query( 'INSERT INTO facebook_campaigns (client_id, account_id, campaign_id, campaign_name, status, effective_status, date_sync) VALUES (:client_id, :account_id, :campaign_id, :campaign_name, NULL, \'ACTIVE\', :date_sync) ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id), account_id = VALUES(account_id), campaign_name = VALUES(campaign_name), effective_status = VALUES(effective_status), date_sync = VALUES(date_sync)', [ ':client_id' => (int) $client_id, ':account_id' => $account_id, ':campaign_id' => $campaign_external_id, ':campaign_name' => $campaign_name, ':date_sync' => $date_sync ] ); $db_id = (int) $mdb -> id(); if ( $db_id > 0 ) { return $db_id; } $existing = $mdb -> query( 'SELECT id FROM facebook_campaigns WHERE client_id = :client_id AND campaign_id = :campaign_id LIMIT 1', [ ':client_id' => (int) $client_id, ':campaign_id' => $campaign_external_id ] ) -> fetchColumn(); return (int) $existing; } static private function upsert_campaign_history( $campaign_db_id, $row, $roas_all_time = 0.0 ) { global $mdb; $date_add = self::normalize_date( $row['date_start'] ?? null ); if ( !$date_add ) { return false; } $spend = self::to_decimal( $row['spend'] ?? 0 ); $conversion_value = self::extract_conversion_value( $row ); $roas = self::extract_roas( $row, $conversion_value, $spend ); $mdb -> query( 'INSERT INTO facebook_campaigns_history (facebook_campaign_id, spend, impressions, clicks, ctr, cpc, conversion_value, roas, roas_all_time, date_add) VALUES (:campaign_id, :spend, :impressions, :clicks, :ctr, :cpc, :conversion_value, :roas, :roas_all_time, :date_add) ON DUPLICATE KEY UPDATE spend = VALUES(spend), impressions = VALUES(impressions), clicks = VALUES(clicks), ctr = VALUES(ctr), cpc = VALUES(cpc), conversion_value = VALUES(conversion_value), roas = VALUES(roas), roas_all_time = VALUES(roas_all_time)', [ ':campaign_id' => (int) $campaign_db_id, ':spend' => $spend, ':impressions' => self::to_int( $row['impressions'] ?? 0 ), ':clicks' => self::to_int( $row['clicks'] ?? 0 ), ':ctr' => self::to_decimal( $row['ctr'] ?? 0 ), ':cpc' => self::to_decimal( $row['cpc'] ?? 0 ), ':conversion_value' => $conversion_value, ':roas' => $roas, ':roas_all_time' => (float) $roas_all_time, ':date_add' => $date_add ] ); return true; } static private function upsert_ad_set( $client_id, $campaign_db_id, $row ) { global $mdb; $ad_set_external_id = self::normalize_external_id( $row['adset_id'] ?? '' ); if ( $ad_set_external_id === null ) { return 0; } $campaign_external_id = self::normalize_external_id( $row['campaign_id'] ?? '' ) ?: '0'; $ad_set_name = trim( (string) ( $row['adset_name'] ?? '' ) ); $date_sync = date( 'Y-m-d H:i:s' ); $mdb -> query( 'INSERT INTO facebook_ad_sets (client_id, facebook_campaign_id, campaign_id, ad_set_id, ad_set_name, status, effective_status, date_sync) VALUES (:client_id, :facebook_campaign_id, :campaign_id, :ad_set_id, :ad_set_name, NULL, \'ACTIVE\', :date_sync) ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id), facebook_campaign_id = VALUES(facebook_campaign_id), campaign_id = VALUES(campaign_id), ad_set_name = VALUES(ad_set_name), effective_status = VALUES(effective_status), date_sync = VALUES(date_sync)', [ ':client_id' => (int) $client_id, ':facebook_campaign_id' => $campaign_db_id > 0 ? (int) $campaign_db_id : null, ':campaign_id' => $campaign_external_id, ':ad_set_id' => $ad_set_external_id, ':ad_set_name' => $ad_set_name, ':date_sync' => $date_sync ] ); $db_id = (int) $mdb -> id(); if ( $db_id > 0 ) { return $db_id; } $existing = $mdb -> query( 'SELECT id FROM facebook_ad_sets WHERE client_id = :client_id AND ad_set_id = :ad_set_id LIMIT 1', [ ':client_id' => (int) $client_id, ':ad_set_id' => $ad_set_external_id ] ) -> fetchColumn(); return (int) $existing; } static private function upsert_ad_set_history( $ad_set_db_id, $row ) { global $mdb; $date_add = self::normalize_date( $row['date_start'] ?? null ); if ( !$date_add ) { return false; } $spend = self::to_decimal( $row['spend'] ?? 0 ); $conversion_value = self::extract_conversion_value( $row ); $roas = self::extract_roas( $row, $conversion_value, $spend ); $mdb -> query( 'INSERT INTO facebook_ad_sets_history (facebook_ad_set_id, spend, impressions, clicks, ctr, cpc, conversion_value, roas, date_add) VALUES (:ad_set_id, :spend, :impressions, :clicks, :ctr, :cpc, :conversion_value, :roas, :date_add) ON DUPLICATE KEY UPDATE spend = VALUES(spend), impressions = VALUES(impressions), clicks = VALUES(clicks), ctr = VALUES(ctr), cpc = VALUES(cpc), conversion_value = VALUES(conversion_value), roas = VALUES(roas)', [ ':ad_set_id' => (int) $ad_set_db_id, ':spend' => $spend, ':impressions' => self::to_int( $row['impressions'] ?? 0 ), ':clicks' => self::to_int( $row['clicks'] ?? 0 ), ':ctr' => self::to_decimal( $row['ctr'] ?? 0 ), ':cpc' => self::to_decimal( $row['cpc'] ?? 0 ), ':conversion_value' => $conversion_value, ':roas' => $roas, ':date_add' => $date_add ] ); return true; } static private function upsert_ad( $client_id, $campaign_db_id, $ad_set_db_id, $row ) { global $mdb; $ad_external_id = self::normalize_external_id( $row['ad_id'] ?? '' ); if ( $ad_external_id === null ) { return 0; } $campaign_external_id = self::normalize_external_id( $row['campaign_id'] ?? '' ) ?: '0'; $ad_set_external_id = self::normalize_external_id( $row['adset_id'] ?? '' ) ?: '0'; $ad_name = trim( (string) ( $row['ad_name'] ?? '' ) ); $date_sync = date( 'Y-m-d H:i:s' ); $mdb -> query( 'INSERT INTO facebook_ads (client_id, facebook_campaign_id, facebook_ad_set_id, campaign_id, ad_set_id, ad_id, ad_name, status, effective_status, date_sync) VALUES (:client_id, :facebook_campaign_id, :facebook_ad_set_id, :campaign_id, :ad_set_id, :ad_id, :ad_name, NULL, \'ACTIVE\', :date_sync) ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id), facebook_campaign_id = VALUES(facebook_campaign_id), facebook_ad_set_id = VALUES(facebook_ad_set_id), campaign_id = VALUES(campaign_id), ad_set_id = VALUES(ad_set_id), ad_name = VALUES(ad_name), effective_status = VALUES(effective_status), date_sync = VALUES(date_sync)', [ ':client_id' => (int) $client_id, ':facebook_campaign_id' => $campaign_db_id > 0 ? (int) $campaign_db_id : null, ':facebook_ad_set_id' => $ad_set_db_id > 0 ? (int) $ad_set_db_id : null, ':campaign_id' => $campaign_external_id, ':ad_set_id' => $ad_set_external_id, ':ad_id' => $ad_external_id, ':ad_name' => $ad_name, ':date_sync' => $date_sync ] ); $db_id = (int) $mdb -> id(); if ( $db_id > 0 ) { return $db_id; } $existing = $mdb -> query( 'SELECT id FROM facebook_ads WHERE client_id = :client_id AND ad_id = :ad_id LIMIT 1', [ ':client_id' => (int) $client_id, ':ad_id' => $ad_external_id ] ) -> fetchColumn(); return (int) $existing; } static private function upsert_ad_history( $ad_db_id, $row ) { global $mdb; $date_add = self::normalize_date( $row['date_start'] ?? null ); if ( !$date_add ) { return false; } $spend = self::to_decimal( $row['spend'] ?? 0 ); $conversion_value = self::extract_conversion_value( $row ); $roas = self::extract_roas( $row, $conversion_value, $spend ); $mdb -> query( 'INSERT INTO facebook_ads_history (facebook_ad_id, spend, impressions, clicks, ctr, cpc, conversion_value, roas, date_add) VALUES (:ad_id, :spend, :impressions, :clicks, :ctr, :cpc, :conversion_value, :roas, :date_add) ON DUPLICATE KEY UPDATE spend = VALUES(spend), impressions = VALUES(impressions), clicks = VALUES(clicks), ctr = VALUES(ctr), cpc = VALUES(cpc), conversion_value = VALUES(conversion_value), roas = VALUES(roas)', [ ':ad_id' => (int) $ad_db_id, ':spend' => $spend, ':impressions' => self::to_int( $row['impressions'] ?? 0 ), ':clicks' => self::to_int( $row['clicks'] ?? 0 ), ':ctr' => self::to_decimal( $row['ctr'] ?? 0 ), ':cpc' => self::to_decimal( $row['cpc'] ?? 0 ), ':conversion_value' => $conversion_value, ':roas' => $roas, ':date_add' => $date_add ] ); return true; } static private function resolve_campaign_db_id( $client_id, $row, &$campaign_map ) { $campaign_external_id = self::normalize_external_id( $row['campaign_id'] ?? '' ); if ( $campaign_external_id === null ) { return 0; } if ( isset( $campaign_map[ $campaign_external_id ] ) ) { return (int) $campaign_map[ $campaign_external_id ]; } $campaign_db_id = self::upsert_campaign( $client_id, $row ); if ( $campaign_db_id > 0 ) { $campaign_map[ $campaign_external_id ] = $campaign_db_id; } return $campaign_db_id; } static private function resolve_ad_set_db_id( $client_id, $campaign_db_id, $row, &$ad_set_map ) { $ad_set_external_id = self::normalize_external_id( $row['adset_id'] ?? '' ); if ( $ad_set_external_id === null ) { return 0; } if ( isset( $ad_set_map[ $ad_set_external_id ] ) ) { return (int) $ad_set_map[ $ad_set_external_id ]; } $ad_set_db_id = self::upsert_ad_set( $client_id, $campaign_db_id, $row ); if ( $ad_set_db_id > 0 ) { $ad_set_map[ $ad_set_external_id ] = $ad_set_db_id; } return $ad_set_db_id; } static private function normalize_external_id( $value ) { $digits = preg_replace( '/\D+/', '', (string) $value ); if ( $digits === '' ) { return null; } return $digits; } static private function normalize_date( $value ) { $value = trim( (string) $value ); if ( $value === '' ) { return null; } $timestamp = strtotime( $value ); if ( !$timestamp ) { return null; } return date( 'Y-m-d', $timestamp ); } static private function to_decimal( $value ) { if ( is_string( $value ) ) { $value = str_replace( ',', '.', $value ); } return (float) $value; } static private function to_int( $value ) { return (int) round( (float) $value ); } static private function get_purchase_action_types_priority() { return [ 'purchase', 'omni_purchase', 'offsite_conversion.fb_pixel_purchase', 'web_in_store_purchase', 'onsite_conversion.purchase', 'app_custom_event.fb_mobile_purchase' ]; } static private function get_metric_map( $raw ) { $map = []; if ( !is_array( $raw ) ) { return $map; } foreach ( $raw as $row ) { if ( !is_array( $row ) ) { continue; } $action_type = trim( (string) ( $row['action_type'] ?? '' ) ); if ( $action_type === '' ) { continue; } $map[ $action_type ] = self::to_decimal( $row['value'] ?? 0 ); } return $map; } static private function extract_conversion_value( $row ) { if ( !is_array( $row ) ) { return 0.0; } if ( isset( $row['conversion_value'] ) ) { return self::to_decimal( $row['conversion_value'] ); } $action_values_map = self::get_metric_map( $row['action_values'] ?? [] ); if ( empty( $action_values_map ) ) { return 0.0; } foreach ( self::get_purchase_action_types_priority() as $action_type ) { if ( array_key_exists( $action_type, $action_values_map ) ) { return self::to_decimal( $action_values_map[ $action_type ] ); } } return 0.0; } static private function extract_roas( $row, $conversion_value, $spend ) { if ( !is_array( $row ) ) { return 0.0; } $purchase_roas_map = self::get_metric_map( $row['purchase_roas'] ?? [] ); foreach ( self::get_purchase_action_types_priority() as $action_type ) { if ( array_key_exists( $action_type, $purchase_roas_map ) ) { return self::to_decimal( $purchase_roas_map[ $action_type ] ) * 100; } } $conversion_value = self::to_decimal( $conversion_value ); $spend = self::to_decimal( $spend ); if ( $conversion_value > 0 && $spend > 0 ) { return round( ( $conversion_value / $spend ) * 100, 6 ); } return 0.0; } static private function get_level_config( $level ) { $level = strtolower( trim( (string) $level ) ); $map = [ 'campaign' => [ 'base_table' => 'facebook_campaigns', 'history_table' => 'facebook_campaigns_history', 'fk_column' => 'facebook_campaign_id', 'name_column' => 'campaign_name', 'external_id_column' => 'campaign_id' ], 'adset' => [ 'base_table' => 'facebook_ad_sets', 'history_table' => 'facebook_ad_sets_history', 'fk_column' => 'facebook_ad_set_id', 'name_column' => 'ad_set_name', 'external_id_column' => 'ad_set_id' ], 'ad' => [ 'base_table' => 'facebook_ads', 'history_table' => 'facebook_ads_history', 'fk_column' => 'facebook_ad_id', 'name_column' => 'ad_name', 'external_id_column' => 'ad_id' ] ]; return $map[ $level ] ?? null; } static public function get_clients_for_reports() { return self::get_clients_for_sync( 0 ); } static public function get_entities_with_latest_metrics( $client_id, $level, $filters = [] ) { global $mdb; $cfg = self::get_level_config( $level ); if ( !$cfg ) { return []; } $client_id = (int) $client_id; if ( $client_id <= 0 ) { return []; } $filters = is_array( $filters ) ? $filters : []; $campaign_id = (int) ( $filters['campaign_id'] ?? 0 ); $ad_set_id = (int) ( $filters['ad_set_id'] ?? 0 ); $extra_where = ''; $params = [ ':client_id' => $client_id ]; if ( $level === 'adset' && $campaign_id > 0 ) { $extra_where .= ' AND b.facebook_campaign_id = :campaign_id'; $params[':campaign_id'] = $campaign_id; } if ( $level === 'ad' ) { if ( $campaign_id > 0 ) { $extra_where .= ' AND b.facebook_campaign_id = :campaign_id'; $params[':campaign_id'] = $campaign_id; } if ( $ad_set_id > 0 ) { $extra_where .= ' AND b.facebook_ad_set_id = :ad_set_id'; $params[':ad_set_id'] = $ad_set_id; } } $sql = 'SELECT b.id, b.' . $cfg['external_id_column'] . ' AS external_id, b.' . $cfg['name_column'] . ' AS entity_name, h.date_add, h.spend, h.impressions, h.clicks, h.ctr, h.cpc, h.conversion_value, h.roas FROM ' . $cfg['base_table'] . ' b LEFT JOIN ' . $cfg['history_table'] . ' h ON h.' . $cfg['fk_column'] . ' = b.id AND h.date_add = ( SELECT MAX(h2.date_add) FROM ' . $cfg['history_table'] . ' h2 WHERE h2.' . $cfg['fk_column'] . ' = b.id ) WHERE b.client_id = :client_id ' . $extra_where . ' ORDER BY b.' . $cfg['name_column'] . ' ASC'; return $mdb -> query( $sql, $params ) -> fetchAll( \PDO::FETCH_ASSOC ); } static public function get_entity_history( $level, $entity_id, $start, $length, $revert = false ) { global $mdb; $cfg = self::get_level_config( $level ); if ( !$cfg ) { return []; } $entity_id = (int) $entity_id; if ( $entity_id <= 0 ) { return []; } $start = max( 0, (int) $start ); $length = max( 1, min( 2000, (int) $length ) ); $order = $revert ? 'ASC' : 'DESC'; $columns = 'date_add, spend, impressions, clicks, ctr, cpc, conversion_value, roas'; if ( $level === 'campaign' ) { $columns .= ', roas_all_time'; } $sql = 'SELECT ' . $columns . ' FROM ' . $cfg['history_table'] . ' WHERE ' . $cfg['fk_column'] . ' = :entity_id ORDER BY date_add ' . $order . ' LIMIT ' . $start . ', ' . $length; return $mdb -> query( $sql, [ ':entity_id' => $entity_id ] ) -> fetchAll( \PDO::FETCH_ASSOC ); } static public function get_entity_history_total( $level, $entity_id ) { global $mdb; $cfg = self::get_level_config( $level ); if ( !$cfg ) { return 0; } $entity_id = (int) $entity_id; if ( $entity_id <= 0 ) { return 0; } $count = $mdb -> query( 'SELECT COUNT(*) FROM ' . $cfg['history_table'] . ' WHERE ' . $cfg['fk_column'] . ' = :entity_id', [ ':entity_id' => $entity_id ] ) -> fetchColumn(); return (int) $count; } }