diff --git a/autoload/controls/class.Cron.php b/autoload/controls/class.Cron.php index 200e94a..b01a3a2 100644 --- a/autoload/controls/class.Cron.php +++ b/autoload/controls/class.Cron.php @@ -14,6 +14,8 @@ class Cron } $date = \S::get( 'date' ) ? date( 'Y-m-d', strtotime( \S::get( 'date' ) ) ) : date( 'Y-m-d', strtotime( '-1 days' ) ); + $conversion_window_days = self::get_conversion_window_days(); + $import_dates = self::build_backfill_dates( $date, $conversion_window_days ); $client_id = (int) \S::get( 'client_id' ); if ( $client_id > 0 ) @@ -38,6 +40,7 @@ class Cron 'result' => empty( $sync['errors'] ) ? 'Synchronizacja produktow zakonczona.' : 'Synchronizacja produktow zakonczona z bledami.', 'client_id' => (int) $client['id'], 'date' => $date, + 'active_date' => $date, 'phase' => 'single_full', 'processed_products' => (int) $sync['processed_products'], 'skipped' => (int) $sync['skipped'], @@ -58,13 +61,16 @@ class Cron } $state_key = 'cron_products_pipeline_state'; - $state = self::get_products_pipeline_state( $state_key, $date, $client_ids ); + $state = self::get_products_pipeline_state( $state_key, $date, $client_ids, $import_dates ); if ( $state['phase'] === 'done' ) { echo json_encode( [ - 'result' => 'Pipeline cron_products jest zakonczony dla tej daty.', - 'date' => $state['import_date'], + 'result' => 'Pipeline cron_products jest zakonczony dla calego okna dat.', + 'date' => $date, + 'active_date' => $state['import_date'], + 'conversion_window_days' => (int) ( $state['conversion_window_days'] ?? $conversion_window_days ), + 'dates_synced' => $state['import_dates'] ?? $import_dates, 'phase' => 'done', 'total_clients' => count( $client_ids ) ] ); @@ -80,7 +86,7 @@ class Cron $done_key = $phase_map[ $state['phase'] ] ?? null; if ( !$done_key ) { - $state = self::init_products_pipeline_state( $date, $client_ids ); + $state = self::init_products_pipeline_state( $date, $client_ids, $import_dates ); $done_key = 'fetch_done_ids'; } @@ -93,7 +99,9 @@ class Cron echo json_encode( [ 'result' => 'Faza zakonczona. Przejdz do nastepnej fazy kolejnym wywolaniem.', - 'date' => $state['import_date'], + 'date' => $date, + 'active_date' => $state['import_date'], + 'conversion_window_days' => (int) ( $state['conversion_window_days'] ?? $conversion_window_days ), 'phase' => $state['phase'], 'total_clients' => count( $client_ids ) ] ); @@ -109,7 +117,10 @@ class Cron $response = [ 'result' => '', - 'date' => $state['import_date'], + 'date' => $date, + 'active_date' => $state['import_date'], + 'conversion_window_days' => (int) ( $state['conversion_window_days'] ?? $conversion_window_days ), + 'dates_synced' => $state['import_dates'] ?? $import_dates, 'phase' => $state['phase'], 'client_id' => (int) $selected_client['id'], 'errors' => [] @@ -151,11 +162,24 @@ class Cron exit; } - static private function init_products_pipeline_state( $date, $client_ids ) + static private function init_products_pipeline_state( $date, $client_ids, $import_dates ) { + $import_dates = array_values( array_unique( array_map( function( $item ) + { + return date( 'Y-m-d', strtotime( $item ) ); + }, (array) $import_dates ) ) ); + sort( $import_dates ); + + $anchor_date = date( 'Y-m-d', strtotime( $date ) ); + $initial_date = !empty( $import_dates ) ? $import_dates[0] : $anchor_date; + return [ - 'import_date' => date( 'Y-m-d', strtotime( $date ) ), - 'clients_hash' => md5( implode( ',', $client_ids ) ), + 'anchor_date' => $anchor_date, + 'import_date' => $initial_date, + 'import_dates' => $import_dates, + 'current_date_index' => 0, + 'conversion_window_days' => count( $import_dates ), + 'clients_hash' => md5( $anchor_date . '|' . implode( ',', $client_ids ) . '|' . implode( ',', $import_dates ) ), 'phase' => 'fetch', 'fetch_done_ids' => [], 'aggregate_30_done_ids' => [], @@ -163,24 +187,40 @@ class Cron ]; } - static private function get_products_pipeline_state( $state_key, $date, $client_ids ) + static private function get_products_pipeline_state( $state_key, $date, $client_ids, $import_dates ) { $state_raw = self::get_setting_value( $state_key, '' ); $state = json_decode( (string) $state_raw, true ); $expected_date = date( 'Y-m-d', strtotime( $date ) ); - $expected_hash = md5( implode( ',', $client_ids ) ); + $expected_dates = array_values( array_unique( array_map( function( $item ) + { + return date( 'Y-m-d', strtotime( $item ) ); + }, (array) $import_dates ) ) ); + sort( $expected_dates ); + $expected_hash = md5( $expected_date . '|' . implode( ',', $client_ids ) . '|' . implode( ',', $expected_dates ) ); if ( !is_array( $state ) ) { - return self::init_products_pipeline_state( $expected_date, $client_ids ); + return self::init_products_pipeline_state( $expected_date, $client_ids, $expected_dates ); } - if ( ( $state['import_date'] ?? '' ) !== $expected_date || ( $state['clients_hash'] ?? '' ) !== $expected_hash ) + if ( ( $state['anchor_date'] ?? '' ) !== $expected_date || ( $state['clients_hash'] ?? '' ) !== $expected_hash ) { - return self::init_products_pipeline_state( $expected_date, $client_ids ); + return self::init_products_pipeline_state( $expected_date, $client_ids, $expected_dates ); } + if ( !isset( $state['import_dates'] ) || !is_array( $state['import_dates'] ) || empty( $state['import_dates'] ) ) + { + $state['import_dates'] = $expected_dates; + } + if ( !isset( $state['current_date_index'] ) || !is_numeric( $state['current_date_index'] ) ) + { + $state['current_date_index'] = 0; + } + $state['current_date_index'] = max( 0, min( count( $state['import_dates'] ) - 1, (int) $state['current_date_index'] ) ); + $state['import_date'] = $state['import_dates'][ $state['current_date_index'] ] ?? $expected_date; + foreach ( [ 'fetch_done_ids', 'aggregate_30_done_ids', 'aggregate_temp_done_ids' ] as $key ) { if ( !isset( $state[ $key ] ) || !is_array( $state[ $key ] ) ) @@ -217,6 +257,21 @@ class Cron return $state; } + $current_date_index = (int) ( $state['current_date_index'] ?? 0 ); + $import_dates = is_array( $state['import_dates'] ?? null ) ? $state['import_dates'] : []; + $last_index = count( $import_dates ) - 1; + + if ( $last_index >= 0 && $current_date_index < $last_index ) + { + $state['current_date_index'] = $current_date_index + 1; + $state['import_date'] = $import_dates[ $state['current_date_index'] ]; + $state['phase'] = 'fetch'; + $state['fetch_done_ids'] = []; + $state['aggregate_30_done_ids'] = []; + $state['aggregate_temp_done_ids'] = []; + return $state; + } + $state['phase'] = 'done'; return $state; } @@ -542,22 +597,27 @@ class Cron return (int) $mdb -> id(); } - static private function aggregate_products_history_30_for_client( $client_id, $date ) + static private function aggregate_products_history_30_for_client( $client_id, $date = null ) { global $mdb; $client_id = (int) $client_id; - $date = date( 'Y-m-d', strtotime( $date ) ); + $params = [ ':client_id' => $client_id ]; + $sql = 'SELECT DISTINCT ph.product_id, ph.campaign_id, ph.ad_group_id, ph.date_add + FROM products_history AS ph + INNER JOIN products AS p ON p.id = ph.product_id + WHERE p.client_id = :client_id + AND ph.updated = 1'; - $rows = $mdb -> query( - 'SELECT DISTINCT ph.product_id, ph.campaign_id, ph.ad_group_id, ph.date_add - FROM products_history AS ph - INNER JOIN products AS p ON p.id = ph.product_id - WHERE p.client_id = ' . $client_id . ' - AND ph.updated = 1 - AND ph.date_add = \'' . $date . '\' - ORDER BY ph.product_id ASC, ph.campaign_id ASC, ph.ad_group_id ASC' - ) -> fetchAll( \PDO::FETCH_ASSOC ); + if ( $date ) + { + $params[':date_add'] = date( 'Y-m-d', strtotime( $date ) ); + $sql .= ' AND ph.date_add = :date_add'; + } + + $sql .= ' ORDER BY ph.date_add ASC, ph.product_id ASC, ph.campaign_id ASC, ph.ad_group_id ASC'; + + $rows = $mdb -> query( $sql, $params ) -> fetchAll( \PDO::FETCH_ASSOC ); $processed = 0; foreach ( $rows as $row ) @@ -1178,6 +1238,9 @@ class Cron exit; } + $sync_date = \S::get( 'date' ) ? date( 'Y-m-d', strtotime( \S::get( 'date' ) ) ) : date( 'Y-m-d' ); + $conversion_window_days = self::get_conversion_window_days(); + $sync_dates = self::build_backfill_dates( $sync_date, $conversion_window_days ); $client_id = (int) \S::get( 'client_id' ); if ( $client_id > 0 ) @@ -1194,11 +1257,13 @@ class Cron exit; } - $sync = self::sync_campaigns_for_client( $client, $api ); + $sync = self::sync_campaigns_for_client( $client, $api, $sync_date, true ); echo json_encode( [ 'result' => empty( $sync['errors'] ) ? 'Synchronizacja kampanii zakonczona.' : 'Synchronizacja kampanii zakonczona z bledami.', 'client_id' => (int) $client['id'], + 'date' => $sync_date, + 'active_date' => $sync_date, 'processed_records' => (int) $sync['processed_records'], 'ad_groups_synced' => (int) ( $sync['ad_groups_synced'] ?? 0 ), 'search_terms_synced' => (int) ( $sync['search_terms_synced'] ?? 0 ), @@ -1239,15 +1304,44 @@ class Cron $client_ids = array_values( array_unique( $client_ids ) ); + $window_state_key = 'cron_campaigns_window_state'; + $window_state = self::get_campaigns_window_state( $window_state_key, $sync_date, $sync_dates ); + self::save_campaigns_window_state( $window_state_key, $window_state ); + $active_sync_date = $window_state['sync_date']; + $sync_details = ( $active_sync_date === $sync_date ); + $state_key = 'cron_campaigns_state'; - $state = self::get_daily_cron_state( $state_key ); + $state = self::get_daily_cron_state( $state_key, $active_sync_date ); $next_client_id = self::pick_next_client_id( $client_ids, $state['processed_ids'] ); if ( !$next_client_id ) { + $previous_index = (int) ( $window_state['current_date_index'] ?? 0 ); + $window_state = self::advance_campaigns_window_state( $window_state ); + $next_index = (int) ( $window_state['current_date_index'] ?? 0 ); + + if ( $next_index !== $previous_index ) + { + self::save_campaigns_window_state( $window_state_key, $window_state ); + echo json_encode( [ + 'result' => 'Wszyscy klienci dla aktualnego dnia zostali przetworzeni. Kolejne wywolanie przejdzie do nastepnego dnia.', + 'date' => $sync_date, + 'active_date' => $active_sync_date, + 'next_active_date' => $window_state['sync_date'], + 'conversion_window_days' => $conversion_window_days, + 'dates_synced' => $window_state['sync_dates'], + 'processed_clients' => count( array_intersect( $client_ids, $state['processed_ids'] ) ), + 'total_clients' => count( $client_ids ) + ] ); + exit; + } + echo json_encode( [ - 'result' => 'Wszyscy klienci kampanii zostali juz dzis przetworzeni.', - 'date' => $state['date'], + 'result' => 'Wszyscy klienci kampanii zostali juz przetworzeni dla calego okna dat.', + 'date' => $sync_date, + 'active_date' => $active_sync_date, + 'conversion_window_days' => $conversion_window_days, + 'dates_synced' => $window_state['sync_dates'], 'processed_clients' => count( array_intersect( $client_ids, $state['processed_ids'] ) ), 'total_clients' => count( $client_ids ) ] ); @@ -1261,18 +1355,22 @@ class Cron exit; } - $sync = self::sync_campaigns_for_client( $selected_client, $api ); + $sync = self::sync_campaigns_for_client( $selected_client, $api, $active_sync_date, $sync_details ); // Oznaczamy klienta jako przetworzonego rowniez po bledzie, aby nie zapetlac wywolan. $state['processed_ids'][] = (int) $next_client_id; $state['processed_ids'] = array_values( array_unique( array_map( 'intval', $state['processed_ids'] ) ) ); - self::save_daily_cron_state( $state_key, $state ); + self::save_daily_cron_state( $state_key, $state, $active_sync_date ); $processed_today = count( array_intersect( $client_ids, $state['processed_ids'] ) ); echo json_encode( [ 'result' => empty( $sync['errors'] ) ? 'Synchronizacja kampanii zakonczona.' : 'Synchronizacja kampanii zakonczona z bledami.', 'client_id' => $next_client_id, + 'date' => $sync_date, + 'active_date' => $active_sync_date, + 'conversion_window_days' => $conversion_window_days, + 'dates_synced' => $window_state['sync_dates'], 'processed_records' => (int) $sync['processed_records'], 'ad_groups_synced' => (int) ( $sync['ad_groups_synced'] ?? 0 ), 'search_terms_synced' => (int) ( $sync['search_terms_synced'] ?? 0 ), @@ -1284,17 +1382,18 @@ class Cron exit; } - static private function sync_campaigns_for_client( $client, $api ) + static private function sync_campaigns_for_client( $client, $api, $as_of_date = null, $sync_details = true ) { global $mdb; - $today = date( 'Y-m-d' ); + $as_of_date = $as_of_date ? date( 'Y-m-d', strtotime( $as_of_date ) ) : date( 'Y-m-d' ); + $sync_details = (bool) $sync_details; $processed = 0; $errors = []; $customer_id = $client['google_ads_customer_id']; $campaigns_db_map = []; - $campaigns_30 = $api -> get_campaigns_30_days( $customer_id ); + $campaigns_30 = $api -> get_campaigns_30_days( $customer_id, $as_of_date ); if ( $campaigns_30 === false ) { $last_err = \services\GoogleAdsApi::get_setting( 'google_ads_last_error' ); @@ -1314,7 +1413,21 @@ class Cron $campaigns_30 = []; } - $campaigns_all_time = $api -> get_campaigns_all_time( $customer_id ); + $campaigns_all_time = $api -> get_campaigns_all_time( $customer_id, $as_of_date ); + if ( $campaigns_all_time === false ) + { + $last_err = \services\GoogleAdsApi::get_setting( 'google_ads_last_error' ); + $errors[] = 'Blad pobierania danych all time dla klienta ' . $client['name'] . ' (ID: ' . $customer_id . '): ' . $last_err; + + return [ + 'processed_records' => 0, + 'ad_groups_synced' => 0, + 'search_terms_synced' => 0, + 'negative_keywords_synced' => 0, + 'errors' => $errors + ]; + } + $all_time_map = []; $all_time_totals = [ 'cost' => 0.0, @@ -1325,7 +1438,7 @@ class Cron { foreach ( $campaigns_all_time as $cat ) { - $all_time_map[ $cat['campaign_id'] ] = $cat['roas_all_time']; + $all_time_map[ (string) ( $cat['campaign_id'] ?? '' ) ] = (float) ( $cat['roas_all_time'] ?? 0 ); $all_time_totals['cost'] += (float) ( $cat['cost_all_time'] ?? 0 ); $all_time_totals['conversion_value'] += (float) ( $cat['conversion_value_all_time'] ?? 0 ); } @@ -1393,18 +1506,18 @@ class Cron if ( $mdb -> count( 'campaigns_history', [ 'AND' => [ 'campaign_id' => $db_campaign_id, - 'date_add' => $today + 'date_add' => $as_of_date ] ] ) ) { $mdb -> update( 'campaigns_history', $history_data, [ 'AND' => [ 'campaign_id' => $db_campaign_id, - 'date_add' => $today + 'date_add' => $as_of_date ] ] ); } else { $history_data['campaign_id'] = $db_campaign_id; - $history_data['date_add'] = $today; + $history_data['date_add'] = $as_of_date; $mdb -> insert( 'campaigns_history', $history_data ); } @@ -1457,26 +1570,37 @@ class Cron if ( $mdb -> count( 'campaigns_history', [ 'AND' => [ 'campaign_id' => $db_account_campaign_id, - 'date_add' => $today + 'date_add' => $as_of_date ] ] ) ) { $mdb -> update( 'campaigns_history', $account_history_data, [ 'AND' => [ 'campaign_id' => $db_account_campaign_id, - 'date_add' => $today + 'date_add' => $as_of_date ] ] ); } else { $account_history_data['campaign_id'] = $db_account_campaign_id; - $account_history_data['date_add'] = $today; + $account_history_data['date_add'] = $as_of_date; $mdb -> insert( 'campaigns_history', $account_history_data ); } $processed++; - $ad_groups_sync = self::sync_campaign_ad_groups_for_client( $campaigns_db_map, $customer_id, $api, $today ); - $search_terms_sync = self::sync_campaign_search_terms_for_client( $campaigns_db_map, $ad_groups_sync['ad_group_map'], $customer_id, $api, $today ); - $negative_keywords_sync = self::sync_campaign_negative_keywords_for_client( $campaigns_db_map, $ad_groups_sync['ad_group_map'], $customer_id, $api, $today ); + if ( !$sync_details ) + { + return [ + 'processed_records' => $processed, + 'ad_groups_synced' => 0, + 'search_terms_synced' => 0, + 'negative_keywords_synced' => 0, + 'errors' => $errors + ]; + } + + $ad_groups_sync = self::sync_campaign_ad_groups_for_client( $campaigns_db_map, $customer_id, $api, $as_of_date ); + $search_terms_sync = self::sync_campaign_search_terms_for_client( $campaigns_db_map, $ad_groups_sync['ad_group_map'], $customer_id, $api, $as_of_date ); + $negative_keywords_sync = self::sync_campaign_negative_keywords_for_client( $campaigns_db_map, $ad_groups_sync['ad_group_map'], $customer_id, $api, $as_of_date ); $errors = array_merge( $errors, $ad_groups_sync['errors'], $search_terms_sync['errors'], $negative_keywords_sync['errors'] ); @@ -1856,17 +1980,104 @@ class Cron return [ 'count' => $count, 'errors' => [] ]; } - static private function get_daily_cron_state( $state_key ) + static private function get_campaigns_window_state( $state_key, $anchor_date, $sync_dates ) { - $today = date( 'Y-m-d' ); + $anchor_date = date( 'Y-m-d', strtotime( $anchor_date ) ); + $sync_dates = array_values( array_unique( array_map( function( $item ) + { + return date( 'Y-m-d', strtotime( $item ) ); + }, (array) $sync_dates ) ) ); + sort( $sync_dates ); + + if ( empty( $sync_dates ) ) + { + $sync_dates = [ $anchor_date ]; + } + + $expected_hash = md5( $anchor_date . '|' . implode( ',', $sync_dates ) ); + $state_raw = self::get_setting_value( $state_key, '' ); + $state = json_decode( (string) $state_raw, true ); + + if ( !is_array( $state ) || ( $state['window_hash'] ?? '' ) !== $expected_hash ) + { + return [ + 'anchor_date' => $anchor_date, + 'sync_dates' => $sync_dates, + 'current_date_index' => 0, + 'sync_date' => $sync_dates[0], + 'window_hash' => $expected_hash + ]; + } + + $current_date_index = (int) ( $state['current_date_index'] ?? 0 ); + $current_date_index = max( 0, min( count( $sync_dates ) - 1, $current_date_index ) ); + + return [ + 'anchor_date' => $anchor_date, + 'sync_dates' => $sync_dates, + 'current_date_index' => $current_date_index, + 'sync_date' => $sync_dates[ $current_date_index ], + 'window_hash' => $expected_hash + ]; + } + + static private function save_campaigns_window_state( $state_key, $state ) + { + $sync_dates = array_values( array_unique( array_map( function( $item ) + { + return date( 'Y-m-d', strtotime( $item ) ); + }, (array) ( $state['sync_dates'] ?? [] ) ) ) ); + sort( $sync_dates ); + + if ( empty( $sync_dates ) ) + { + return; + } + + $anchor_date = date( 'Y-m-d', strtotime( $state['anchor_date'] ?? end( $sync_dates ) ) ); + $current_date_index = max( 0, min( count( $sync_dates ) - 1, (int) ( $state['current_date_index'] ?? 0 ) ) ); + $payload = [ + 'anchor_date' => $anchor_date, + 'sync_dates' => $sync_dates, + 'current_date_index' => $current_date_index, + 'sync_date' => $sync_dates[ $current_date_index ], + 'window_hash' => md5( $anchor_date . '|' . implode( ',', $sync_dates ) ) + ]; + + self::set_setting_value( $state_key, json_encode( $payload, JSON_UNESCAPED_UNICODE ) ); + } + + static private function advance_campaigns_window_state( $state ) + { + $sync_dates = is_array( $state['sync_dates'] ?? null ) ? $state['sync_dates'] : []; + $current_date_index = (int) ( $state['current_date_index'] ?? 0 ); + $last_index = count( $sync_dates ) - 1; + + if ( $last_index < 0 ) + { + return $state; + } + + if ( $current_date_index < $last_index ) + { + $state['current_date_index'] = $current_date_index + 1; + $state['sync_date'] = $sync_dates[ $state['current_date_index'] ]; + } + + return $state; + } + + static private function get_daily_cron_state( $state_key, $state_date = null ) + { + $state_date = $state_date ? date( 'Y-m-d', strtotime( $state_date ) ) : date( 'Y-m-d' ); $state_raw = self::get_setting_value( $state_key, '' ); $state = json_decode( (string) $state_raw, true ); - if ( !is_array( $state ) || ( $state['date'] ?? '' ) !== $today ) + if ( !is_array( $state ) || ( $state['date'] ?? '' ) !== $state_date ) { return [ - 'date' => $today, + 'date' => $state_date, 'processed_ids' => [] ]; } @@ -1885,15 +2096,16 @@ class Cron } return [ - 'date' => $today, + 'date' => $state_date, 'processed_ids' => array_values( array_unique( $processed_ids ) ) ]; } - static private function save_daily_cron_state( $state_key, $state ) + static private function save_daily_cron_state( $state_key, $state, $state_date = null ) { + $state_date = $state_date ? date( 'Y-m-d', strtotime( $state_date ) ) : date( 'Y-m-d' ); $payload = [ - 'date' => date( 'Y-m-d' ), + 'date' => $state_date, 'processed_ids' => array_values( array_unique( array_map( 'intval', $state['processed_ids'] ?? [] ) ) ) ]; @@ -1924,6 +2136,42 @@ class Cron return 0; } + static private function get_conversion_window_days() + { + $request_value = (int) \S::get( 'conversion_window_days' ); + if ( $request_value > 0 ) + { + return min( 90, $request_value ); + } + + $setting_value = (int) self::get_setting_value( 'google_ads_conversion_window_days', 7 ); + if ( $setting_value <= 0 ) + { + return 7; + } + + return min( 90, $setting_value ); + } + + static private function build_backfill_dates( $end_date, $window_days ) + { + $end_timestamp = strtotime( $end_date ); + if ( !$end_timestamp ) + { + $end_timestamp = strtotime( date( 'Y-m-d' ) ); + } + + $window_days = max( 1, min( 90, (int) $window_days ) ); + $dates = []; + + for ( $i = $window_days - 1; $i >= 0; $i-- ) + { + $dates[] = date( 'Y-m-d', strtotime( '-' . $i . ' days', $end_timestamp ) ); + } + + return $dates; + } + static private function get_setting_value( $setting_key, $default = null ) { global $mdb; diff --git a/autoload/services/class.GoogleAdsApi.php b/autoload/services/class.GoogleAdsApi.php index ceb2e67..fc4f373 100644 --- a/autoload/services/class.GoogleAdsApi.php +++ b/autoload/services/class.GoogleAdsApi.php @@ -465,100 +465,177 @@ class GoogleAdsApi . "FROM shopping_performance_view " . "WHERE segments.date = '" . $date . "'"; - $results = $this -> search_stream( $customer_id, $gaql_with_ad_group ); - $fallback_without_ad_group = false; + $gaql_pmax_asset_group = "SELECT " + . "segments.date, " + . "segments.product_item_id, " + . "segments.product_title, " + . "campaign.id, " + . "campaign.name, " + . "asset_group.id, " + . "asset_group.name, " + . "metrics.impressions, " + . "metrics.clicks, " + . "metrics.cost_micros, " + . "metrics.conversions, " + . "metrics.conversions_value " + . "FROM asset_group_product_group_view " + . "WHERE segments.date = '" . $date . "' " + . "AND campaign.advertising_channel_type = 'PERFORMANCE_MAX'"; - if ( $results === false ) + $gaql_pmax_campaign_level_fallback = "SELECT " + . "segments.date, " + . "segments.product_item_id, " + . "segments.product_title, " + . "campaign.id, " + . "campaign.name, " + . "campaign.advertising_channel_type, " + . "metrics.impressions, " + . "metrics.clicks, " + . "metrics.cost_micros, " + . "metrics.conversions, " + . "metrics.conversions_value " + . "FROM shopping_performance_view " + . "WHERE segments.date = '" . $date . "'"; + + $results_with_ad_group = $this -> search_stream( $customer_id, $gaql_with_ad_group ); + $results_pmax_asset_group = $this -> search_stream( $customer_id, $gaql_pmax_asset_group ); + $results_pmax_campaign_fallback = []; + + $had_success = false; + + if ( is_array( $results_with_ad_group ) ) { - $gaql_without_ad_group = "SELECT " - . "segments.date, " - . "segments.product_item_id, " - . "segments.product_title, " - . "campaign.id, " - . "campaign.name, " - . "metrics.impressions, " - . "metrics.clicks, " - . "metrics.cost_micros, " - . "metrics.conversions, " - . "metrics.conversions_value " - . "FROM shopping_performance_view " - . "WHERE segments.date = '" . $date . "'"; + $had_success = true; + } + else + { + $results_with_ad_group = []; + } - $results = $this -> search_stream( $customer_id, $gaql_without_ad_group ); - if ( $results === false ) + if ( is_array( $results_pmax_asset_group ) ) + { + $had_success = true; + } + else + { + $results_pmax_asset_group = []; + + // Fallback dla kont/API, gdzie asset_group_product_group_view moze nie byc dostepny. + $tmp = $this -> search_stream( $customer_id, $gaql_pmax_campaign_level_fallback ); + if ( is_array( $tmp ) ) { - return false; + $had_success = true; + foreach ( $tmp as $row ) + { + $channel = (string) ( $row['campaign']['advertisingChannelType'] ?? '' ); + if ( strtoupper( $channel ) === 'PERFORMANCE_MAX' ) + { + $results_pmax_campaign_fallback[] = $row; + } + } } + } - $fallback_without_ad_group = true; + if ( !$had_success ) + { + return false; } $products = []; - foreach ( $results as $row ) + $collect_rows = function( $rows, $group_kind ) use ( &$products ) { - $offer_id = trim( (string) ( $row['segments']['productItemId'] ?? '' ) ); - if ( $offer_id === '' ) + if ( !is_array( $rows ) ) { - continue; + return; } - $campaign_id = (int) ( $row['campaign']['id'] ?? 0 ); - $campaign_name = trim( (string) ( $row['campaign']['name'] ?? '' ) ); - if ( $campaign_name === '' && $campaign_id > 0 ) + foreach ( $rows as $row ) { - $campaign_name = 'Kampania #' . $campaign_id; - } - - $ad_group_id = 0; - $ad_group_name = 'PMax (bez grup reklam)'; - - if ( !$fallback_without_ad_group ) - { - $ad_group_id = (int) ( $row['adGroup']['id'] ?? 0 ); - $ad_group_name = trim( (string) ( $row['adGroup']['name'] ?? '' ) ); - - if ( $ad_group_id > 0 && $ad_group_name === '' ) + $offer_id = trim( (string) ( $row['segments']['productItemId'] ?? '' ) ); + if ( $offer_id === '' ) { - $ad_group_name = 'Ad group #' . $ad_group_id; + continue; } - else if ( $ad_group_id <= 0 ) + + $campaign_id = (int) ( $row['campaign']['id'] ?? 0 ); + $campaign_name = trim( (string) ( $row['campaign']['name'] ?? '' ) ); + if ( $campaign_name === '' && $campaign_id > 0 ) { - $ad_group_name = 'PMax (bez grup reklam)'; + $campaign_name = 'Kampania #' . $campaign_id; } + + $ad_group_id = 0; + $ad_group_name = 'PMax (bez grup reklam)'; + + if ( $group_kind === 'ad_group' ) + { + $ad_group_id = (int) ( $row['adGroup']['id'] ?? 0 ); + $ad_group_name = trim( (string) ( $row['adGroup']['name'] ?? '' ) ); + + if ( $ad_group_id > 0 && $ad_group_name === '' ) + { + $ad_group_name = 'Ad group #' . $ad_group_id; + } + else if ( $ad_group_id <= 0 ) + { + $ad_group_name = 'PMax (bez grup reklam)'; + } + } + else if ( $group_kind === 'asset_group' ) + { + $ad_group_id = (int) ( $row['assetGroup']['id'] ?? 0 ); + $ad_group_name = trim( (string) ( $row['assetGroup']['name'] ?? '' ) ); + + if ( $ad_group_id > 0 && $ad_group_name === '' ) + { + $ad_group_name = 'Asset group #' . $ad_group_id; + } + else if ( $ad_group_id <= 0 ) + { + $ad_group_name = 'PMax (bez grup plikow)'; + } + } + + $scope_key = $offer_id . '|' . $campaign_id . '|' . $ad_group_id; + + if ( !isset( $products[ $scope_key ] ) ) + { + $products[ $scope_key ] = [ + 'OfferId' => $offer_id, + 'ProductTitle' => (string) ( $row['segments']['productTitle'] ?? $offer_id ), + 'CampaignId' => $campaign_id, + 'CampaignName' => $campaign_name, + 'AdGroupId' => $ad_group_id, + 'AdGroupName' => $ad_group_name, + 'Impressions' => 0, + 'Clicks' => 0, + 'Cost' => 0.0, + 'Conversions' => 0.0, + 'ConversionValue' => 0.0 + ]; + } + + $products[ $scope_key ]['Impressions'] += (int) ( $row['metrics']['impressions'] ?? 0 ); + $products[ $scope_key ]['Clicks'] += (int) ( $row['metrics']['clicks'] ?? 0 ); + $products[ $scope_key ]['Cost'] += (float) ( $row['metrics']['costMicros'] ?? 0 ) / 1000000; + $products[ $scope_key ]['Conversions'] += (float) ( $row['metrics']['conversions'] ?? 0 ); + $products[ $scope_key ]['ConversionValue'] += (float) ( $row['metrics']['conversionsValue'] ?? 0 ); } + }; - $scope_key = $offer_id . '|' . $campaign_id . '|' . $ad_group_id; - - if ( !isset( $products[ $scope_key ] ) ) - { - $products[ $scope_key ] = [ - 'OfferId' => $offer_id, - 'ProductTitle' => (string) ( $row['segments']['productTitle'] ?? $offer_id ), - 'CampaignId' => $campaign_id, - 'CampaignName' => $campaign_name, - 'AdGroupId' => $ad_group_id, - 'AdGroupName' => $ad_group_name, - 'Impressions' => 0, - 'Clicks' => 0, - 'Cost' => 0.0, - 'Conversions' => 0.0, - 'ConversionValue' => 0.0 - ]; - } - - $products[ $scope_key ]['Impressions'] += (int) ( $row['metrics']['impressions'] ?? 0 ); - $products[ $scope_key ]['Clicks'] += (int) ( $row['metrics']['clicks'] ?? 0 ); - $products[ $scope_key ]['Cost'] += (float) ( $row['metrics']['costMicros'] ?? 0 ) / 1000000; - $products[ $scope_key ]['Conversions'] += (float) ( $row['metrics']['conversions'] ?? 0 ); - $products[ $scope_key ]['ConversionValue'] += (float) ( $row['metrics']['conversionsValue'] ?? 0 ); - } + $collect_rows( $results_with_ad_group, 'ad_group' ); + $collect_rows( $results_pmax_asset_group, 'asset_group' ); + $collect_rows( $results_pmax_campaign_fallback, 'campaign' ); return array_values( $products ); } - public function get_campaigns_30_days( $customer_id ) + public function get_campaigns_30_days( $customer_id, $as_of_date = null ) { + $as_of_date = $as_of_date ? date( 'Y-m-d', strtotime( $as_of_date ) ) : date( 'Y-m-d' ); + $date_from = date( 'Y-m-d', strtotime( '-29 days', strtotime( $as_of_date ) ) ); + $gaql = "SELECT " . "campaign.id, " . "campaign.name, " @@ -570,7 +647,8 @@ class GoogleAdsApi . "metrics.conversions_value " . "FROM campaign " . "WHERE campaign.status = 'ENABLED' " - . "AND segments.date DURING LAST_30_DAYS"; + . "AND segments.date >= '" . $date_from . "' " + . "AND segments.date <= '" . $as_of_date . "'"; $results = $this -> search_stream( $customer_id, $gaql ); if ( $results === false ) return false; @@ -619,16 +697,34 @@ class GoogleAdsApi // --- Kampanie: dane all-time --- - public function get_campaigns_all_time( $customer_id ) + public function get_campaigns_all_time( $customer_id, $as_of_date = null ) { - $gaql = "SELECT " - . "campaign.id, " - . "metrics.cost_micros, " - . "metrics.conversions_value " - . "FROM campaign " - . "WHERE campaign.status = 'ENABLED'"; + $as_of_date = $as_of_date ? date( 'Y-m-d', strtotime( $as_of_date ) ) : null; + + $gaql_base = "SELECT " + . "campaign.id, " + . "metrics.cost_micros, " + . "metrics.conversions_value " + . "FROM campaign " + . "WHERE campaign.status = 'ENABLED'"; + + $results = false; + if ( $as_of_date ) + { + $gaql_with_date = $gaql_base . " AND segments.date <= '" . $as_of_date . "'"; + $results = $this -> search_stream( $customer_id, $gaql_with_date ); + + // Fallback do starego sposobu, gdy filtr daty nie jest akceptowany na danym koncie. + if ( $results === false ) + { + $results = $this -> search_stream( $customer_id, $gaql_base ); + } + } + else + { + $results = $this -> search_stream( $customer_id, $gaql_base ); + } - $results = $this -> search_stream( $customer_id, $gaql ); if ( $results === false ) return false; $campaigns = []; @@ -637,18 +733,33 @@ class GoogleAdsApi $cid = $row['campaign']['id'] ?? null; if ( !$cid ) continue; - $cost = (float) ( $row['metrics']['costMicros'] ?? 0 ) / 1000000; - $value = (float) ( $row['metrics']['conversionsValue'] ?? 0 ); + if ( !isset( $campaigns[ $cid ] ) ) + { + $campaigns[ $cid ] = [ + 'campaign_id' => $cid, + 'cost_micros_total' => 0.0, + 'conversion_value_total' => 0.0 + ]; + } - $campaigns[] = [ - 'campaign_id' => $cid, - 'cost_all_time' => $cost, - 'conversion_value_all_time' => $value, - 'roas_all_time' => ( $cost > 0 ) ? round( ( $value / $cost ) * 100, 2 ) : 0, + $campaigns[ $cid ]['cost_micros_total'] += (float) ( $row['metrics']['costMicros'] ?? 0 ); + $campaigns[ $cid ]['conversion_value_total'] += (float) ( $row['metrics']['conversionsValue'] ?? 0 ); + } + + foreach ( $campaigns as &$campaign ) + { + $cost = $campaign['cost_micros_total'] / 1000000; + $value = $campaign['conversion_value_total']; + + $campaign = [ + 'campaign_id' => $campaign['campaign_id'], + 'cost_all_time' => $cost, + 'conversion_value_all_time' => $value, + 'roas_all_time' => ( $cost > 0 ) ? round( ( $value / $cost ) * 100, 2 ) : 0 ]; } - return $campaigns; + return array_values( $campaigns ); } public function get_ad_groups_30_days( $customer_id ) diff --git a/templates/products/main_view.php b/templates/products/main_view.php index ad09d02..515e0ff 100644 --- a/templates/products/main_view.php +++ b/templates/products/main_view.php @@ -30,6 +30,13 @@ +