is_configured() ) { echo json_encode( [ 'result' => 'Google Ads API nie jest skonfigurowane. Uzupelnij dane w Ustawieniach.' ] ); exit; } $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 ) { $client = $mdb -> get( 'clients', '*', [ 'AND' => [ 'id' => $client_id, 'google_ads_customer_id[!]' => null, 'deleted' => 0 ] ] ); if ( !$client || trim( (string) $client['google_ads_customer_id'] ) === '' ) { echo json_encode( [ 'result' => 'Nie znaleziono klienta z poprawnym Google Ads Customer ID.', 'client_id' => $client_id ] ); exit; } $sync = self::sync_products_fetch_for_client( $client, $api, $date ); $history_30 = self::aggregate_products_history_30_for_client( (int) $client['id'], $date ); $temp_rows = self::rebuild_products_temp_for_client( (int) $client['id'] ); echo json_encode( [ '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'], 'fetch_skipped_reason' => isset( $sync['fetch_skipped_reason'] ) ? (string) $sync['fetch_skipped_reason'] : '', 'history_30_products' => (int) $history_30, 'products_temp_rows' => (int) $temp_rows, 'errors' => $sync['errors'] ] ); exit; } self::cleanup_old_sync_rows( 30 ); $client_ids = $mdb -> query( "SELECT c.id FROM clients c WHERE c.deleted = 0 AND c.google_ads_customer_id IS NOT NULL AND c.google_ads_customer_id <> '' AND ( NOT EXISTS ( SELECT 1 FROM campaigns cp WHERE cp.client_id = c.id ) OR EXISTS ( SELECT 1 FROM campaigns cp WHERE cp.client_id = c.id AND cp.campaign_id > 0 AND ( cp.campaign_name IS NULL OR cp.campaign_name <> '--- konto ---' ) ) ) ORDER BY c.id ASC" ) -> fetchAll( \PDO::FETCH_COLUMN ); $client_ids = array_values( array_unique( array_map( 'intval', $client_ids ) ) ); if ( empty( $client_ids ) ) { echo json_encode( [ 'result' => 'Brak klientow z ustawionym Google Ads Customer ID do przetworzenia.', 'processed_clients' => 0, 'errors' => [] ] ); exit; } self::ensure_sync_rows( 'products', $import_dates, $client_ids ); $active_client_id = self::get_active_client( 'products' ); if ( !$active_client_id ) { echo json_encode( [ 'result' => 'Pipeline cron_products jest zakonczony dla calego okna dat.', 'date' => $date, 'conversion_window_days' => $conversion_window_days, 'dates_synced' => $import_dates, 'phase' => 'done', 'total_clients' => count( $client_ids ) ] ); exit; } $source_phase = self::determine_client_products_phase( $active_client_id ); if ( !$source_phase ) { echo json_encode( [ 'result' => 'Brak dat do przetworzenia dla klienta ID: ' . $active_client_id, 'date' => $date, 'active_client_id' => $active_client_id, 'phase' => 'done', 'total_clients' => count( $client_ids ) ] ); exit; } $phase_next_map = [ 'pending' => 'fetch', 'fetch' => 'aggregate_30', 'aggregate_30' => 'done' ]; $next_phase = $phase_next_map[ $source_phase ] ?? 'done'; $dates_per_run_default = (int) ( $settings['cron_products_clients_per_run'] ?? 10 ); if ( $dates_per_run_default <= 0 ) { $dates_per_run_default = 10; } $dates_per_run = (int) \S::get( 'clients_per_run' ); if ( $dates_per_run <= 0 ) { $dates_per_run = $dates_per_run_default; } $dates_per_run = min( 100, $dates_per_run ); $dates_batch = self::get_pending_dates_for_client( 'products', $active_client_id, $source_phase, $dates_per_run ); if ( empty( $dates_batch ) ) { echo json_encode( [ 'result' => 'Faza zakonczona dla klienta. Kolejne wywolanie przejdzie do nastepnej fazy lub klienta.', 'date' => $date, 'active_client_id' => $active_client_id, 'conversion_window_days' => $conversion_window_days, 'phase' => $source_phase, 'total_clients' => count( $client_ids ) ] ); exit; } $selected_client = $mdb -> get( 'clients', '*', [ 'id' => $active_client_id ] ); if ( !$selected_client ) { echo json_encode( [ 'result' => 'Nie udalo sie znalezc klienta do synchronizacji produktow. ID: ' . $active_client_id, 'active_client_id' => $active_client_id, 'errors' => [ 'Klient ID ' . $active_client_id . ' nie znaleziony.' ] ] ); exit; } $dates_processed_in_call = []; $errors = []; $processed_products_total = 0; $skipped_total = 0; $fetch_skipped_reasons = []; $history_30_products_total = 0; $products_temp_rows_total = 0; if ( $source_phase === 'aggregate_30' ) { $products_temp_rows_total += (int) self::rebuild_products_temp_for_client( $active_client_id ); foreach ( $dates_batch as $active_date ) { self::mark_sync_phase( 'products', $active_date, $active_client_id, 'done' ); $dates_processed_in_call[] = $active_date; } } else { foreach ( $dates_batch as $active_date ) { $error_msg = null; if ( $source_phase === 'pending' ) { $sync = self::sync_products_fetch_for_client( $selected_client, $api, $active_date ); $processed_products_total += (int) ( $sync['processed_products'] ?? 0 ); $skipped_total += (int) ( $sync['skipped'] ?? 0 ); $fetch_skipped_reason = trim( (string) ( $sync['fetch_skipped_reason'] ?? '' ) ); if ( $fetch_skipped_reason !== '' ) { $fetch_skipped_reasons[ $fetch_skipped_reason ] = true; } if ( !empty( $sync['errors'] ) ) { $errors = array_merge( $errors, (array) $sync['errors'] ); $error_msg = implode( '; ', (array) $sync['errors'] ); } } else if ( $source_phase === 'fetch' ) { $history_30_products_total += (int) self::aggregate_products_history_30_for_client( $active_client_id, $active_date ); } self::mark_sync_phase( 'products', $active_date, $active_client_id, $next_phase, $error_msg ); $dates_processed_in_call[] = $active_date; } } $done_count = (int) $mdb -> query( "SELECT COUNT(*) FROM cron_sync_status cs INNER JOIN clients c ON cs.client_id = c.id AND c.deleted = 0 WHERE cs.pipeline = 'products' AND cs.client_id = :client_id AND cs.phase = 'done' AND cs.sync_date >= DATE_SUB(CURDATE(), INTERVAL 90 DAY)", [ ':client_id' => $active_client_id ] ) -> fetchColumn(); $total_count = (int) $mdb -> query( "SELECT COUNT(*) FROM cron_sync_status cs INNER JOIN clients c ON cs.client_id = c.id AND c.deleted = 0 WHERE cs.pipeline = 'products' AND cs.client_id = :client_id AND cs.sync_date >= DATE_SUB(CURDATE(), INTERVAL 90 DAY)", [ ':client_id' => $active_client_id ] ) -> fetchColumn(); $remaining_dates = max( 0, $total_count - $done_count ); $estimated_calls_remaining = (int) ceil( $remaining_dates / max( 1, $dates_per_run ) ); $phase_labels = [ 'pending' => 'fetch', 'fetch' => 'aggregate_30', 'aggregate_30' => 'aggregate_temp' ]; $display_phase = $phase_labels[ $source_phase ] ?? $source_phase; $result_text = 'Brak dat do przetworzenia w tej fazie.'; if ( $source_phase === 'pending' ) { $result_text = empty( $errors ) ? 'Pobieranie produktow zakonczone.' : 'Pobieranie produktow zakonczone z bledami.'; } else if ( $source_phase === 'fetch' ) { $result_text = 'Pierwsza agregacja (history_30) zakonczona.'; } else if ( $source_phase === 'aggregate_30' ) { $result_text = 'Druga agregacja (products_temp) zakonczona.'; } echo json_encode( [ 'result' => $result_text, 'date' => $date, 'active_client_id' => $active_client_id, 'conversion_window_days' => $conversion_window_days, 'dates_synced' => $import_dates, 'phase' => $display_phase, 'dates_per_run' => $dates_per_run, 'dates_processed_in_call' => count( $dates_processed_in_call ), 'processed_dates' => $dates_processed_in_call, 'remaining_dates' => $remaining_dates, 'estimated_calls_remaining' => $estimated_calls_remaining, 'total_clients' => count( $client_ids ), 'processed_products' => $processed_products_total, 'skipped' => $skipped_total, 'fetch_skipped_reasons' => array_keys( $fetch_skipped_reasons ), 'history_30_products' => $history_30_products_total, 'products_temp_rows' => $products_temp_rows_total, 'errors' => $errors ] ); exit; } static public function cron_products_urls() { global $mdb, $settings; self::touch_cron_invocation( __FUNCTION__ ); $api = new \services\GoogleAdsApi(); if ( !$api -> is_merchant_configured() ) { echo json_encode( [ 'result' => 'Merchant API nie jest skonfigurowane. Uzupelnij OAuth2 Client ID/Secret oraz Merchant Refresh Token w Ustawieniach.' ] ); exit; } $client_id = (int) \S::get( 'client_id' ); $batch_limit = (int) \S::get( 'limit' ); $debug_mode = (int) \S::get( 'debug' ) === 1; if ( $batch_limit <= 0 ) { $batch_limit = (int) ( $settings['cron_products_urls_limit_per_client'] ?? 100 ); } if ( $batch_limit <= 0 ) { $batch_limit = 100; } $batch_limit = min( 1000, $batch_limit ); $clients_per_run_default = (int) ( $settings['cron_products_urls_clients_per_run'] ?? ( $settings['cron_products_clients_per_run'] ?? 1 ) ); if ( $clients_per_run_default <= 0 ) { $clients_per_run_default = 1; } $clients_per_run = (int) \S::get( 'clients_per_run' ); if ( $clients_per_run <= 0 ) { $clients_per_run = (int) self::get_setting_value( 'cron_products_urls_clients_per_run', $clients_per_run_default ); } if ( $clients_per_run <= 0 ) { $clients_per_run = $clients_per_run_default; } $clients_per_run = min( 20, $clients_per_run ); $where = "deleted = 0 AND google_merchant_account_id IS NOT NULL AND google_merchant_account_id <> ''"; if ( $client_id > 0 ) { $where .= ' AND id = ' . $client_id; } $clients = $mdb -> query( 'SELECT id, name, google_merchant_account_id FROM clients WHERE ' . $where . ' ORDER BY id ASC' ) -> fetchAll( \PDO::FETCH_ASSOC ); if ( !is_array( $clients ) || empty( $clients ) ) { echo json_encode( [ 'result' => 'Brak klientow z ustawionym Merchant Account ID.', 'processed_clients' => 0, 'checked_products' => 0, 'updated_urls' => 0, 'errors' => [] ] ); exit; } $total_clients_available = count( $clients ); if ( $client_id <= 0 ) { $last_client_cursor = (int) self::get_setting_value( 'cron_products_urls_last_client_id', 0 ); $clients = self::pick_clients_batch_by_cursor( $clients, $clients_per_run, $last_client_cursor ); } else { $clients_per_run = 1; } $checked_products = 0; $updated_urls = 0; $unresolved_products = 0; $processed_clients = 0; $errors = []; $details = []; foreach ( $clients as $client ) { $processed_clients++; $selected_products = self::get_products_missing_url_for_client( (int) $client['id'], $batch_limit ); $product_count = count( $selected_products ); $diag = $debug_mode ? self::get_products_url_sync_diagnostics_for_client( (int) $client['id'] ) : null; if ( $product_count === 0 ) { $detail_row = [ 'client_id' => (int) $client['id'], 'client_name' => (string) $client['name'], 'merchant_account_id' => (string) $client['google_merchant_account_id'], 'selected_products' => 0, 'updated_urls' => 0, 'unresolved_products' => 0 ]; if ( $debug_mode ) { $detail_row['diag'] = $diag; } $details[] = $detail_row; continue; } $checked_products += $product_count; $offer_ids = []; foreach ( $selected_products as $row ) { $offer_ids[] = (string) $row['offer_id']; } $links_map = $api -> get_merchant_product_links_for_offer_ids( (string) $client['google_merchant_account_id'], $offer_ids ); if ( $links_map === false ) { $last_err = (string) \services\GoogleAdsApi::get_setting( 'google_merchant_last_error' ); $errors[] = 'Blad Merchant API dla klienta ' . $client['name'] . ' (ID: ' . $client['id'] . '): ' . $last_err; $unresolved_products += $product_count; $detail_row = [ 'client_id' => (int) $client['id'], 'client_name' => (string) $client['name'], 'merchant_account_id' => (string) $client['google_merchant_account_id'], 'selected_products' => $product_count, 'updated_urls' => 0, 'unresolved_products' => $product_count ]; if ( $debug_mode ) { $detail_row['diag'] = $diag; } $details[] = $detail_row; continue; } $client_updated = 0; foreach ( $selected_products as $row ) { $offer_id = (string) $row['offer_id']; if ( !isset( $links_map[ $offer_id ] ) ) { continue; } \factory\Products::set_product_data( (int) $row['product_id'], 'product_url', (string) $links_map[ $offer_id ] ); $client_updated++; } $updated_urls += $client_updated; $client_unresolved = max( 0, $product_count - $client_updated ); $unresolved_products += $client_unresolved; $detail_row = [ 'client_id' => (int) $client['id'], 'client_name' => (string) $client['name'], 'merchant_account_id' => (string) $client['google_merchant_account_id'], 'selected_products' => $product_count, 'updated_urls' => $client_updated, 'unresolved_products' => $client_unresolved ]; if ( $debug_mode ) { $detail_row['diag'] = $diag; } $details[] = $detail_row; } if ( $client_id <= 0 && !empty( $clients ) ) { $last_client = end( $clients ); $last_client_id = (int) ( $last_client['id'] ?? 0 ); if ( $last_client_id > 0 ) { self::set_setting_value( 'cron_products_urls_last_client_id', (string) $last_client_id ); } } echo json_encode( [ 'result' => empty( $errors ) ? 'Synchronizacja URL produktow zakonczona.' : 'Synchronizacja URL produktow zakonczona z bledami.', 'total_clients_available' => $total_clients_available, 'processed_clients' => $processed_clients, 'clients_per_run' => $clients_per_run, 'checked_products' => $checked_products, 'updated_urls' => $updated_urls, 'unresolved_products' => $unresolved_products, 'errors' => $errors, 'details' => $details ] ); exit; } static private function get_products_url_sync_diagnostics_for_client( $client_id ) { global $mdb; $client_id = (int) $client_id; if ( $client_id <= 0 ) { return []; } $diag = []; $diag['products_total'] = (int) $mdb -> query( 'SELECT COUNT(*) FROM products WHERE client_id = ' . $client_id ) -> fetchColumn(); $diag['products_not_deleted'] = (int) $mdb -> query( 'SELECT COUNT(*) FROM products WHERE client_id = ' . $client_id ) -> fetchColumn(); $diag['products_with_offer_id'] = (int) $mdb -> query( 'SELECT COUNT(*) FROM products WHERE client_id = ' . $client_id . ' AND TRIM( COALESCE( offer_id, "" ) ) <> ""' ) -> fetchColumn(); $diag['products_with_pd_rows'] = (int) $mdb -> query( 'SELECT COUNT( DISTINCT pd.product_id ) FROM products_data pd INNER JOIN products p ON p.id = pd.product_id WHERE p.client_id = ' . $client_id ) -> fetchColumn(); $diag['products_with_real_url'] = (int) $mdb -> query( 'SELECT COUNT(*) FROM products p LEFT JOIN ( SELECT product_id, MAX( CASE WHEN TRIM( COALESCE( product_url, \"\" ) ) = \"\" THEN 0 WHEN LOWER( TRIM( product_url ) ) IN ( \"0\", \"-\", \"null\" ) THEN 0 ELSE 1 END ) AS has_real_url FROM products_data GROUP BY product_id ) pd ON pd.product_id = p.id WHERE p.client_id = ' . $client_id . ' AND TRIM( COALESCE( p.offer_id, \"\" ) ) <> \"\" AND COALESCE( pd.has_real_url, 0 ) = 1' ) -> fetchColumn(); $diag['products_missing_url'] = (int) $mdb -> query( 'SELECT COUNT(*) FROM products p LEFT JOIN ( SELECT product_id, MAX( CASE WHEN TRIM( COALESCE( product_url, \"\" ) ) = \"\" THEN 0 WHEN LOWER( TRIM( product_url ) ) IN ( \"0\", \"-\", \"null\" ) THEN 0 ELSE 1 END ) AS has_real_url FROM products_data GROUP BY product_id ) pd ON pd.product_id = p.id WHERE p.client_id = ' . $client_id . ' AND TRIM( COALESCE( p.offer_id, \"\" ) ) <> \"\" AND COALESCE( pd.has_real_url, 0 ) = 0' ) -> fetchColumn(); return $diag; } static private function get_products_missing_url_for_client( $client_id, $limit ) { global $mdb; $client_id = (int) $client_id; $limit = max( 1, min( 1000, (int) $limit ) ); if ( $client_id <= 0 ) { return []; } $sql = 'SELECT p.id AS product_id, p.offer_id ' . 'FROM products p ' . 'LEFT JOIN ( ' . ' SELECT product_id, ' . ' MAX( CASE ' . ' WHEN TRIM( COALESCE( product_url, \'\' ) ) = \'\' THEN 0 ' . ' WHEN LOWER( TRIM( product_url ) ) IN ( \'0\', \'-\', \'null\' ) THEN 0 ' . ' ELSE 1 ' . ' END ) AS has_real_url ' . ' FROM products_data ' . ' GROUP BY product_id ' . ') pd ON pd.product_id = p.id ' . 'WHERE p.client_id = ' . $client_id . ' ' . 'AND TRIM( COALESCE( p.offer_id, \'\' ) ) <> \'\' ' . 'AND COALESCE( pd.has_real_url, 0 ) = 0 ' . 'ORDER BY p.id ASC ' . 'LIMIT ' . $limit; $rows = $mdb -> query( $sql ) -> fetchAll( \PDO::FETCH_ASSOC ); return is_array( $rows ) ? $rows : []; } static private function sync_products_fetch_for_client( $client, $api, $date ) { global $mdb; $client_id = (int) $client['id']; $customer_id = trim( (string) ( $client['google_ads_customer_id'] ?? '' ) ); $date = date( 'Y-m-d', strtotime( $date ) ); $known_campaign_types = $mdb -> query( 'SELECT DISTINCT UPPER( TRIM( advertising_channel_type ) ) AS channel_type FROM campaigns WHERE client_id = :client_id AND campaign_id > 0 AND advertising_channel_type IS NOT NULL AND TRIM( advertising_channel_type ) <> ""', [ ':client_id' => $client_id ] ) -> fetchAll( \PDO::FETCH_COLUMN ); $known_campaign_types = array_values( array_unique( array_filter( array_map( function( $item ) { return strtoupper( trim( (string) $item ) ); }, (array) $known_campaign_types ) ) ) ); $product_campaign_types = [ 'SHOPPING', 'PERFORMANCE_MAX' ]; $has_product_campaign_type = count( array_intersect( $known_campaign_types, $product_campaign_types ) ) > 0; if ( !empty( $known_campaign_types ) && !$has_product_campaign_type ) { return [ 'date' => $date, 'processed_products' => 0, 'skipped' => 0, 'history_30_products' => 0, 'products_temp_rows' => 0, 'touched_products' => 0, 'fetch_skipped_reason' => 'non_product_campaign_types', 'errors' => [] ]; } $products = $api -> get_products_for_date( $customer_id, $date ); if ( $products === false ) { $last_err = \services\GoogleAdsApi::get_setting( 'google_ads_last_error' ); return [ 'date' => $date, 'processed_products' => 0, 'skipped' => 0, 'history_30_products' => 0, 'products_temp_rows' => 0, 'errors' => [ 'Blad API dla klienta ' . $client['name'] . ' (ID: ' . $customer_id . '): ' . $last_err ] ]; } if ( !is_array( $products ) ) { $products = []; } $existing_products_rows = $mdb -> query( 'SELECT id, offer_id, name FROM products WHERE client_id = :client_id ORDER BY id ASC', [ ':client_id' => $client_id ] ) -> fetchAll( \PDO::FETCH_ASSOC ); $products_by_offer_id = []; foreach ( $existing_products_rows as $row ) { $offer_id = trim( (string) ( $row['offer_id'] ?? '' ) ); if ( $offer_id === '' || isset( $products_by_offer_id[ $offer_id ] ) ) { continue; } $products_by_offer_id[ $offer_id ] = [ 'id' => (int) ( $row['id'] ?? 0 ), 'name' => (string) ( $row['name'] ?? '' ) ]; } $products_data_rows = $mdb -> query( 'SELECT pd.id, pd.product_id, pd.product_url FROM products_data AS pd INNER JOIN products AS p ON p.id = pd.product_id WHERE p.client_id = :client_id', [ ':client_id' => $client_id ] ) -> fetchAll( \PDO::FETCH_ASSOC ); $products_data_map = []; foreach ( $products_data_rows as $row ) { $product_id = (int) ( $row['product_id'] ?? 0 ); if ( $product_id <= 0 || isset( $products_data_map[ $product_id ] ) ) { continue; } $products_data_map[ $product_id ] = [ 'exists' => true, 'product_url' => trim( (string) ( $row['product_url'] ?? '' ) ) ]; } $existing_campaigns_rows = $mdb -> query( 'SELECT id, campaign_id, campaign_name FROM campaigns WHERE client_id = :client_id AND campaign_id > 0', [ ':client_id' => $client_id ] ) -> fetchAll( \PDO::FETCH_ASSOC ); $campaigns_by_external_id = []; $campaigns_by_db_id = []; foreach ( $existing_campaigns_rows as $row ) { $db_campaign_id = (int) ( $row['id'] ?? 0 ); $external_campaign_id = (int) ( $row['campaign_id'] ?? 0 ); if ( $db_campaign_id <= 0 ) { continue; } $campaign_data = [ 'id' => $db_campaign_id, 'campaign_name' => (string) ( $row['campaign_name'] ?? '' ) ]; if ( !isset( $campaigns_by_external_id[ $external_campaign_id ] ) ) { $campaigns_by_external_id[ $external_campaign_id ] = $campaign_data; } $campaigns_by_db_id[ $db_campaign_id ] = $campaign_data; } $existing_campaign_histories = $mdb -> query( 'SELECT ch.campaign_id FROM campaigns_history AS ch INNER JOIN campaigns AS c ON c.id = ch.campaign_id WHERE c.client_id = :client_id AND ch.date_add = :date_add', [ ':client_id' => $client_id, ':date_add' => $date ] ) -> fetchAll( \PDO::FETCH_COLUMN ); $campaign_history_exists = []; foreach ( (array) $existing_campaign_histories as $history_campaign_id ) { $campaign_history_exists[ (int) $history_campaign_id ] = true; } $existing_ad_groups_rows = $mdb -> query( 'SELECT ag.id, ag.campaign_id, ag.ad_group_id, ag.ad_group_name FROM campaign_ad_groups AS ag INNER JOIN campaigns AS c ON c.id = ag.campaign_id WHERE c.client_id = :client_id', [ ':client_id' => $client_id ] ) -> fetchAll( \PDO::FETCH_ASSOC ); $ad_groups_by_scope = []; foreach ( $existing_ad_groups_rows as $row ) { $db_campaign_id = (int) ( $row['campaign_id'] ?? 0 ); $external_ad_group_id = (int) ( $row['ad_group_id'] ?? 0 ); $db_ad_group_id = (int) ( $row['id'] ?? 0 ); if ( $db_campaign_id <= 0 || $db_ad_group_id <= 0 ) { continue; } $scope_key = $db_campaign_id . '|' . $external_ad_group_id; if ( isset( $ad_groups_by_scope[ $scope_key ] ) ) { continue; } $ad_groups_by_scope[ $scope_key ] = [ 'id' => $db_ad_group_id, 'ad_group_name' => (string) ( $row['ad_group_name'] ?? '' ) ]; } $existing_history_rows = $mdb -> query( 'SELECT ph.product_id, ph.campaign_id, ph.ad_group_id, ph.impressions, ph.clicks, ph.cost, ph.conversions, ph.conversions_value FROM products_history AS ph INNER JOIN products AS p ON p.id = ph.product_id WHERE p.client_id = :client_id AND ph.date_add = :date_add', [ ':client_id' => $client_id, ':date_add' => $date ] ) -> fetchAll( \PDO::FETCH_ASSOC ); $history_by_scope = []; foreach ( $existing_history_rows as $row ) { $history_key = (int) ( $row['product_id'] ?? 0 ) . '|' . (int) ( $row['campaign_id'] ?? 0 ) . '|' . (int) ( $row['ad_group_id'] ?? 0 ); $history_by_scope[ $history_key ] = [ 'impressions' => (int) ( $row['impressions'] ?? 0 ), 'clicks' => (int) ( $row['clicks'] ?? 0 ), 'cost' => (float) ( $row['cost'] ?? 0 ), 'conversions' => (float) ( $row['conversions'] ?? 0 ), 'conversions_value' => (float) ( $row['conversions_value'] ?? 0 ) ]; } $resolve_scope_ids = function( $campaign_external_id, $campaign_name, $ad_group_external_id, $ad_group_name ) use ( &$campaigns_by_external_id, &$campaigns_by_db_id, &$campaign_history_exists, &$ad_groups_by_scope, $client_id, $date, $mdb ) { $campaign_external_id = (int) $campaign_external_id; $campaign_name = trim( (string) $campaign_name ); $ad_group_external_id = (int) $ad_group_external_id; $ad_group_name = trim( (string) $ad_group_name ); if ( $campaign_external_id <= 0 ) { return [ 'campaign_id' => 0, 'ad_group_id' => 0 ]; } $campaign_data = $campaigns_by_external_id[ $campaign_external_id ] ?? null; if ( !$campaign_data ) { $campaign_name_to_save = $campaign_name; if ( $campaign_name_to_save === '' ) { $campaign_name_to_save = $campaign_external_id > 0 ? 'Kampania #' . $campaign_external_id : '--- konto ---'; } $mdb -> insert( 'campaigns', [ 'client_id' => $client_id, 'campaign_id' => $campaign_external_id, 'campaign_name' => $campaign_name_to_save ] ); $db_campaign_id = (int) $mdb -> id(); $campaign_data = [ 'id' => $db_campaign_id, 'campaign_name' => $campaign_name_to_save ]; $campaigns_by_external_id[ $campaign_external_id ] = $campaign_data; $campaigns_by_db_id[ $db_campaign_id ] = $campaign_data; } else if ( $campaign_name !== '' && $campaign_name !== (string) ( $campaign_data['campaign_name'] ?? '' ) ) { $mdb -> update( 'campaigns', [ 'campaign_name' => $campaign_name ], [ 'id' => (int) $campaign_data['id'] ] ); $campaign_data['campaign_name'] = $campaign_name; $campaigns_by_external_id[ $campaign_external_id ] = $campaign_data; $campaigns_by_db_id[ (int) $campaign_data['id'] ] = $campaign_data; } $db_campaign_id = (int) ( $campaign_data['id'] ?? 0 ); if ( $db_campaign_id > 0 && !isset( $campaign_history_exists[ $db_campaign_id ] ) ) { $mdb -> insert( 'campaigns_history', [ 'campaign_id' => $db_campaign_id, 'roas_30_days' => 0, 'roas_all_time' => 0, 'budget' => 0, 'money_spent' => 0, 'conversion_value' => 0, 'bidding_strategy' => '', 'date_add' => $date ] ); $campaign_history_exists[ $db_campaign_id ] = true; } if ( $db_campaign_id <= 0 ) { return [ 'campaign_id' => 0, 'ad_group_id' => 0 ]; } if ( $ad_group_external_id <= 0 ) { $scope_key = $db_campaign_id . '|0'; if ( !isset( $ad_groups_by_scope[ $scope_key ] ) ) { $db_ad_group_id = (int) self::ensure_campaign_level_ad_group( $db_campaign_id, $date ); $ad_groups_by_scope[ $scope_key ] = [ 'id' => $db_ad_group_id, 'ad_group_name' => '--- kampania (brak grupy reklam) ---' ]; } return [ 'campaign_id' => $db_campaign_id, 'ad_group_id' => (int) ( $ad_groups_by_scope[ $scope_key ]['id'] ?? 0 ) ]; } $scope_key = $db_campaign_id . '|' . $ad_group_external_id; $ad_group_data = $ad_groups_by_scope[ $scope_key ] ?? null; if ( !$ad_group_data ) { $ad_group_name_to_save = $ad_group_name !== '' ? $ad_group_name : 'Ad group #' . $ad_group_external_id; $mdb -> insert( 'campaign_ad_groups', [ 'campaign_id' => $db_campaign_id, 'ad_group_id' => $ad_group_external_id, 'ad_group_name' => $ad_group_name_to_save, 'impressions_30' => 0, 'clicks_30' => 0, 'cost_30' => 0, 'conversions_30' => 0, 'conversion_value_30' => 0, 'roas_30' => 0, 'impressions_all_time' => 0, 'clicks_all_time' => 0, 'cost_all_time' => 0, 'conversions_all_time' => 0, 'conversion_value_all_time' => 0, 'roas_all_time' => 0, 'date_sync' => $date ] ); $ad_group_data = [ 'id' => (int) $mdb -> id(), 'ad_group_name' => $ad_group_name_to_save ]; $ad_groups_by_scope[ $scope_key ] = $ad_group_data; } else if ( $ad_group_name !== '' && $ad_group_name !== (string) ( $ad_group_data['ad_group_name'] ?? '' ) ) { $mdb -> update( 'campaign_ad_groups', [ 'ad_group_name' => $ad_group_name ], [ 'id' => (int) $ad_group_data['id'] ] ); $ad_group_data['ad_group_name'] = $ad_group_name; $ad_groups_by_scope[ $scope_key ] = $ad_group_data; } return [ 'campaign_id' => $db_campaign_id, 'ad_group_id' => (int) ( $ad_group_data['id'] ?? 0 ) ]; }; $processed = 0; $skipped = 0; $touched_product_ids = []; foreach ( $products as $offer ) { $offer_external_id = trim( (string) ( $offer['OfferId'] ?? '' ) ); if ( $offer_external_id === '' ) { $skipped++; continue; } $product_title = trim( (string) ( $offer['ProductTitle'] ?? '' ) ); if ( $product_title === '' ) { $product_title = $offer_external_id; } $existing_product = $products_by_offer_id[ $offer_external_id ] ?? null; if ( !$existing_product ) { $mdb -> insert( 'products', [ 'client_id' => $client_id, 'offer_id' => $offer_external_id, 'name' => $product_title ] ); $product_id = $mdb -> id(); $products_by_offer_id[ $offer_external_id ] = [ 'id' => (int) $product_id, 'name' => $product_title ]; } else { $product_id = (int) ( $existing_product['id'] ?? 0 ); $offer_current_name = (string) ( $existing_product['name'] ?? '' ); if ( $offer_current_name != $product_title and $date == date( 'Y-m-d', strtotime( '-1 days' ) ) ) { $mdb -> update( 'products', [ 'name' => $product_title ], [ 'AND' => [ 'client_id' => $client_id, 'offer_id' => $offer_external_id ] ] ); $products_by_offer_id[ $offer_external_id ]['name'] = $product_title; } } if ( !$product_id ) { $skipped++; continue; } $product_url = trim( (string) ( $offer['ProductUrl'] ?? '' ) ); $product_url_path = strtolower( (string) parse_url( $product_url, PHP_URL_PATH ) ); $is_image_url = (bool) preg_match( '/\.(jpg|jpeg|png|gif|webp|bmp|svg|avif)$/i', $product_url_path ); if ( $product_url !== '' && filter_var( $product_url, FILTER_VALIDATE_URL ) && !$is_image_url ) { $product_data_row = $products_data_map[ $product_id ] ?? [ 'exists' => false, 'product_url' => '' ]; $existing_product_url = trim( (string) ( $product_data_row['product_url'] ?? '' ) ); if ( $existing_product_url !== $product_url ) { if ( !empty( $product_data_row['exists'] ) ) { $mdb -> update( 'products_data', [ 'product_url' => $product_url ], [ 'product_id' => $product_id ] ); } else { $mdb -> insert( 'products_data', [ 'product_id' => $product_id, 'product_url' => $product_url ] ); $product_data_row['exists'] = true; } $product_data_row['product_url'] = $product_url; $products_data_map[ $product_id ] = $product_data_row; } } $campaign_external_id = (int) ( $offer['CampaignId'] ?? 0 ); if ( $campaign_external_id <= 0 ) { $skipped++; continue; } $campaign_name = trim( (string) ( $offer['CampaignName'] ?? '' ) ); $ad_group_external_id = (int) ( $offer['AdGroupId'] ?? 0 ); $ad_group_name = trim( (string) ( $offer['AdGroupName'] ?? '' ) ); $scope = $resolve_scope_ids( $campaign_external_id, $campaign_name, $ad_group_external_id, $ad_group_name ); $db_campaign_id = (int) ( $scope['campaign_id'] ?? 0 ); $db_ad_group_id = (int) ( $scope['ad_group_id'] ?? 0 ); $impressions = (int) round( (float) ( $offer['Impressions'] ?? 0 ) ); $clicks = (int) round( (float) ( $offer['Clicks'] ?? 0 ) ); $cost = (float) ( $offer['Cost'] ?? 0 ); $conversions = (float) ( $offer['Conversions'] ?? 0 ); $conversion_value = (float) ( $offer['ConversionValue'] ?? 0 ); $ctr = ( $impressions > 0 ) ? round( $clicks / $impressions, 4 ) * 100 : 0; $offer_data = [ 'impressions' => $impressions, 'clicks' => $clicks, 'ctr' => $ctr, 'cost' => $cost, 'conversions' => $conversions, 'conversions_value' => $conversion_value, 'updated' => 1, 'campaign_id' => $db_campaign_id, 'ad_group_id' => $db_ad_group_id ]; $history_scope_key = (int) $product_id . '|' . (int) $db_campaign_id . '|' . (int) $db_ad_group_id; $offer_data_old = $history_by_scope[ $history_scope_key ] ?? null; if ( $offer_data_old ) { if ( $offer_data_old['impressions'] == $offer_data['impressions'] and $offer_data_old['clicks'] == $offer_data['clicks'] and number_format( (float) str_replace( ',', '.', $offer_data_old['cost'] ), 5 ) == number_format( (float) $offer_data['cost'], 5 ) and (float) $offer_data_old['conversions'] == (float) $offer_data['conversions'] and number_format( (float) str_replace( ',', '.', $offer_data_old['conversions_value'] ), 5 ) == number_format( (float) $offer_data['conversions_value'], 5 ) ) { $touched_product_ids[ $product_id ] = true; $processed++; continue; } $mdb -> update( 'products_history', $offer_data, [ 'AND' => [ 'product_id' => $product_id, 'campaign_id' => $db_campaign_id, 'ad_group_id' => $db_ad_group_id, 'date_add' => $date ] ] ); $history_by_scope[ $history_scope_key ] = [ 'impressions' => $offer_data['impressions'], 'clicks' => $offer_data['clicks'], 'cost' => $offer_data['cost'], 'conversions' => $offer_data['conversions'], 'conversions_value' => $offer_data['conversions_value'] ]; } else { $offer_data['product_id'] = $product_id; $offer_data['date_add'] = $date; $mdb -> insert( 'products_history', $offer_data ); $history_by_scope[ $history_scope_key ] = [ 'impressions' => $offer_data['impressions'], 'clicks' => $offer_data['clicks'], 'cost' => $offer_data['cost'], 'conversions' => $offer_data['conversions'], 'conversions_value' => $offer_data['conversions_value'] ]; } $touched_product_ids[ $product_id ] = true; $processed++; } return [ 'date' => $date, 'processed_products' => $processed, 'skipped' => $skipped, 'touched_products' => count( $touched_product_ids ), 'errors' => [] ]; } static public function resolve_products_scope_ids( $client_id, $campaign_external_id, $campaign_name, $ad_group_external_id, $ad_group_name, $date_sync ) { $client_id = (int) $client_id; $campaign_external_id = (int) $campaign_external_id; $ad_group_external_id = (int) $ad_group_external_id; $db_campaign_id = self::ensure_products_campaign( $client_id, $campaign_external_id, $campaign_name, $date_sync ); if ( $db_campaign_id <= 0 ) { $db_campaign_id = self::ensure_products_campaign( $client_id, 0, '--- konto ---', $date_sync ); } $db_ad_group_id = self::ensure_products_ad_group( $db_campaign_id, $ad_group_external_id, $ad_group_name, $date_sync ); return [ 'campaign_id' => (int) $db_campaign_id, 'ad_group_id' => (int) $db_ad_group_id ]; } static private function ensure_products_campaign( $client_id, $campaign_external_id, $campaign_name, $date_sync ) { global $mdb; $client_id = (int) $client_id; $campaign_external_id = (int) $campaign_external_id; $campaign_name = trim( (string) $campaign_name ); if ( $client_id <= 0 ) { return 0; } $db_campaign_id = (int) $mdb -> get( 'campaigns', 'id', [ 'AND' => [ 'client_id' => $client_id, 'campaign_id' => $campaign_external_id ] ] ); if ( $db_campaign_id > 0 ) { if ( $campaign_name !== '' ) { $mdb -> update( 'campaigns', [ 'campaign_name' => $campaign_name ], [ 'id' => $db_campaign_id ] ); } return $db_campaign_id; } if ( $campaign_name === '' ) { $campaign_name = $campaign_external_id > 0 ? 'Kampania #' . $campaign_external_id : '--- konto ---'; } $mdb -> insert( 'campaigns', [ 'client_id' => $client_id, 'campaign_id' => $campaign_external_id, 'campaign_name' => $campaign_name ] ); $db_campaign_id = (int) $mdb -> id(); if ( $db_campaign_id > 0 && $date_sync ) { if ( !$mdb -> count( 'campaigns_history', [ 'AND' => [ 'campaign_id' => $db_campaign_id, 'date_add' => $date_sync ] ] ) ) { $mdb -> insert( 'campaigns_history', [ 'campaign_id' => $db_campaign_id, 'roas_30_days' => 0, 'roas_all_time' => 0, 'budget' => 0, 'money_spent' => 0, 'conversion_value' => 0, 'bidding_strategy' => '', 'date_add' => $date_sync ] ); } } return $db_campaign_id; } static private function ensure_products_ad_group( $db_campaign_id, $ad_group_external_id, $ad_group_name, $date_sync ) { global $mdb; $db_campaign_id = (int) $db_campaign_id; $ad_group_external_id = (int) $ad_group_external_id; $ad_group_name = trim( (string) $ad_group_name ); if ( $db_campaign_id <= 0 ) { return 0; } if ( $ad_group_external_id <= 0 ) { return (int) self::ensure_campaign_level_ad_group( $db_campaign_id, $date_sync ); } $db_ad_group_id = (int) $mdb -> get( 'campaign_ad_groups', 'id', [ 'AND' => [ 'campaign_id' => $db_campaign_id, 'ad_group_id' => $ad_group_external_id ] ] ); if ( $db_ad_group_id > 0 ) { if ( $ad_group_name !== '' ) { $mdb -> update( 'campaign_ad_groups', [ 'ad_group_name' => $ad_group_name ], [ 'id' => $db_ad_group_id ] ); } return $db_ad_group_id; } if ( $ad_group_name === '' ) { $ad_group_name = 'Ad group #' . $ad_group_external_id; } $mdb -> insert( 'campaign_ad_groups', [ 'campaign_id' => $db_campaign_id, 'ad_group_id' => $ad_group_external_id, 'ad_group_name' => $ad_group_name, 'impressions_30' => 0, 'clicks_30' => 0, 'cost_30' => 0, 'conversions_30' => 0, 'conversion_value_30' => 0, 'roas_30' => 0, 'impressions_all_time' => 0, 'clicks_all_time' => 0, 'cost_all_time' => 0, 'conversions_all_time' => 0, 'conversion_value_all_time' => 0, 'roas_all_time' => 0, 'date_sync' => $date_sync ] ); return (int) $mdb -> id(); } static private function aggregate_products_history_30_for_client( $client_id, $date = null ) { global $mdb; $client_id = (int) $client_id; $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.campaign_id > 0 AND ph.updated = 1'; 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 ) { $product_id = (int) $row['product_id']; $campaign_id = (int) ( $row['campaign_id'] ?? 0 ); $ad_group_id = (int) ( $row['ad_group_id'] ?? 0 ); self::cron_product_history_30_save( $product_id, $row['date_add'], $campaign_id, $ad_group_id ); $mdb -> query( 'UPDATE products_history AS ph INNER JOIN products AS p ON p.id = ph.product_id SET ph.updated = 0 WHERE ph.product_id = :product_id AND ph.campaign_id = :campaign_id AND ph.ad_group_id = :ad_group_id AND ph.date_add = :date_add AND p.client_id = :client_id', [ ':product_id' => $product_id, ':campaign_id' => $campaign_id, ':ad_group_id' => $ad_group_id, ':date_add' => $row['date_add'], ':client_id' => $client_id ] ); $processed++; } return $processed; } static public function rebuild_products_temp_for_client( $client_id ) { global $mdb; $client_id = (int) $client_id; if ( $client_id <= 0 ) { return 0; } $client_bestseller_min_roas = (int) \factory\Products::get_client_bestseller_min_roas( $client_id ); $rows = $mdb -> query( 'SELECT p.id AS product_id, p.name, ph.campaign_id, ph.ad_group_id, COALESCE( SUM( ph.impressions ), 0 ) AS impressions, COALESCE( SUM( ph.clicks ), 0 ) AS clicks, COALESCE( SUM( ph.cost ), 0 ) AS cost, COALESCE( SUM( ph.conversions ), 0 ) AS conversions, COALESCE( SUM( ph.conversions_value ), 0 ) AS conversions_value FROM products AS p LEFT JOIN products_history AS ph ON p.id = ph.product_id AND ph.campaign_id > 0 WHERE p.client_id = :client_id GROUP BY p.id, p.name, ph.campaign_id, ph.ad_group_id', [ ':client_id' => $client_id ] ) -> fetchAll( \PDO::FETCH_ASSOC ); $product_ids = $mdb -> select( 'products', 'id', [ 'client_id' => $client_id ] ); $product_ids = array_values( array_unique( array_map( 'intval', (array) $product_ids ) ) ); if ( !empty( $product_ids ) ) { $mdb -> delete( 'products_temp', [ 'product_id' => $product_ids ] ); } // products_data jest globalne per product_id, wiec klasyfikacje liczymy globalnie. $global_totals = $mdb -> query( 'SELECT p.id AS product_id, COALESCE( SUM( ph.impressions ), 0 ) AS impressions, COALESCE( SUM( ph.clicks ), 0 ) AS clicks, COALESCE( SUM( ph.cost ), 0 ) AS cost, COALESCE( SUM( ph.conversions ), 0 ) AS conversions, COALESCE( SUM( ph.conversions_value ), 0 ) AS conversions_value FROM products AS p LEFT JOIN products_history AS ph ON p.id = ph.product_id AND ph.campaign_id > 0 WHERE p.client_id = :client_id GROUP BY p.id', [ ':client_id' => $client_id ] ) -> fetchAll( \PDO::FETCH_ASSOC ); foreach ( $global_totals as $total ) { $product_id = (int) ( $total['product_id'] ?? 0 ); if ( $product_id <= 0 ) { continue; } $total_cost = (float) ( $total['cost'] ?? 0 ); $total_conversions = (float) ( $total['conversions'] ?? 0 ); $total_conversion_value = (float) ( $total['conversions_value'] ?? 0 ); $total_roas = ( $total_conversions > 0 && $total_cost > 0 ) ? round( $total_conversion_value / $total_cost, 2 ) * 100 : 0; $custom_label_4 = \factory\Products::get_product_data( $product_id, 'custom_label_4' ); if ( $custom_label_4 == null || ( $custom_label_4 == 'bestseller' && $client_bestseller_min_roas > 0 ) ) { $new_custom_label_4 = ( $total_roas > $client_bestseller_min_roas && $total_conversions > 10 ) ? 'bestseller' : null; $offers_data_tmp = $mdb -> get( 'products_data', '*', [ 'product_id' => $product_id ] ); if ( isset( $offers_data_tmp['id'] ) ) { $old_custom_label_4 = (string) ( $offers_data_tmp['custom_label_4'] ?? '' ); if ( $new_custom_label_4 != $offers_data_tmp['custom_label_4'] ) { $mdb -> insert( 'products_comments', [ 'product_id' => $product_id, 'comment' => 'Zmiana pola "custom_label_4" na: ' . $new_custom_label_4, 'type' => 1, 'date_add' => date( 'Y-m-d' ) ] ); } $mdb -> update( 'products_data', [ 'custom_label_4' => $new_custom_label_4 ], [ 'id' => $offers_data_tmp['id'] ] ); if ( $old_custom_label_4 !== (string) $new_custom_label_4 ) { \controls\Products::sync_product_fields_to_merchant( $product_id, [ 'custom_label_4' => [ 'old' => $old_custom_label_4, 'new' => (string) $new_custom_label_4 ] ], 'cron_products' ); } } else { $mdb -> insert( 'products_data', [ 'product_id' => $product_id, 'custom_label_4' => $new_custom_label_4 ] ); if ( $new_custom_label_4 == 'bestseller' ) { $mdb -> insert( 'products_comments', [ 'product_id' => $product_id, 'comment' => 'Zmiana pola "custom_label_4" na: bestseller', 'type' => 1, 'date_add' => date( 'Y-m-d' ) ] ); } \controls\Products::sync_product_fields_to_merchant( $product_id, [ 'custom_label_4' => [ 'old' => '', 'new' => (string) $new_custom_label_4 ] ], 'cron_products' ); } } } $processed_rows = 0; foreach ( $rows as $row ) { $product_id = (int) ( $row['product_id'] ?? 0 ); if ( $product_id <= 0 ) { continue; } $campaign_id = (int) ( $row['campaign_id'] ?? 0 ); $ad_group_id = (int) ( $row['ad_group_id'] ?? 0 ); $impressions = (int) ( $row['impressions'] ?? 0 ); $clicks = (int) ( $row['clicks'] ?? 0 ); $cost = (float) ( $row['cost'] ?? 0 ); $conversions = (float) ( $row['conversions'] ?? 0 ); $conversions_value = (float) ( $row['conversions_value'] ?? 0 ); // Pomijamy puste scope bez danych. if ( $impressions <= 0 && $clicks <= 0 && $cost <= 0 && $conversions <= 0 && $conversions_value <= 0 ) { continue; } $cpc = $clicks > 0 ? round( $cost / $clicks, 6 ) : 0; $roas = ( $conversions > 0 && $cost > 0 ) ? round( $conversions_value / $cost, 2 ) * 100 : 0; $impressions_30 = (int) \factory\Products::get_impressions_30( $product_id, $campaign_id, $ad_group_id ); $clicks_30 = (int) \factory\Products::get_clicks_30( $product_id, $campaign_id, $ad_group_id ); $mdb -> insert( 'products_temp', [ 'product_id' => $product_id, 'campaign_id' => $campaign_id, 'ad_group_id' => $ad_group_id, 'name' => $row['name'], 'impressions' => $impressions, 'impressions_30' => $impressions_30, 'clicks' => $clicks, 'clicks_30' => $clicks_30, 'ctr' => ( $impressions > 0 ) ? round( $clicks / $impressions, 4 ) * 100 : 0, 'cost' => $cost, 'conversions' => $conversions, 'conversions_value' => $conversions_value, 'cpc' => $cpc, 'roas' => $roas, ] ); $processed_rows++; } return $processed_rows; } static public function cron_products_history_30() { global $mdb; self::touch_cron_invocation( __FUNCTION__ ); $start_time = microtime(true); $client_id = \S::get( 'client_id' ); if ( !$client_id ) { echo json_encode( [ 'result' => "Nie podano ID klienta." ] ); exit; } if ( !$mdb -> count( 'clients', [ 'id' => $client_id ] ) ) { echo json_encode( [ 'result' => "Nie znaleziono klienta o podanym ID.", "client" => "Nie istnieje" ] ); exit; } $products = $mdb -> select( 'products', 'id', [ 'client_id' => $client_id ] ); foreach ( $products as $product ) { $scopes = $mdb -> query( 'SELECT DISTINCT campaign_id, ad_group_id, date_add FROM products_history WHERE product_id = ' . $product . ' AND campaign_id > 0 AND updated = 1 ORDER BY date_add DESC' ) -> fetchAll( \PDO::FETCH_ASSOC ); foreach ( $scopes as $scope ) { $campaign_id = (int) ( $scope['campaign_id'] ?? 0 ); $ad_group_id = (int) ( $scope['ad_group_id'] ?? 0 ); $date_add = $scope['date_add'] ?? ''; self::cron_product_history_30_save( $product, $date_add, $campaign_id, $ad_group_id ); $mdb -> update( 'products_history', [ 'updated' => 0 ], [ 'AND' => [ 'product_id' => $product, 'campaign_id' => $campaign_id, 'ad_group_id' => $ad_group_id, 'date_add' => $date_add ] ] ); } } $end_time = microtime(true); $execution_time = $end_time - $start_time; echo json_encode( [ 'result' => "Agregacja zakonczona, dane zapisane do offers_history_30. Czas wykonania skryptu: " . round($execution_time, 4) . " sekund." ] ); exit; } static public function get_roas_all_time( $product_id, $date_to, $campaign_id = 0, $ad_group_id = 0 ) { global $mdb; $product_id = (int) $product_id; $campaign_id = (int) $campaign_id; $ad_group_id = (int) $ad_group_id; $sql = 'SELECT SUM(conversions_value) / SUM(cost) * 100 AS roas_all_time FROM products_history WHERE product_id = :product_id AND date_add <= :date_to AND campaign_id = :campaign_id AND ad_group_id = :ad_group_id'; $roas_all_time = $mdb -> query( $sql, [ ':product_id' => $product_id, ':date_to' => $date_to, ':campaign_id' => $campaign_id, ':ad_group_id' => $ad_group_id ] ) -> fetchColumn(); return round( $roas_all_time, 2 ); } static public function cron_product_history_30_save( $product_id, $date_to, $campaign_id = 0, $ad_group_id = 0 ) { global $mdb; $product_id = (int) $product_id; $campaign_id = (int) $campaign_id; $ad_group_id = (int) $ad_group_id; if ( $campaign_id <= 0 ) { return; } $data = $mdb -> query( 'SELECT date_add, SUM( impressions ) AS impressions, SUM( clicks ) AS clicks, SUM( cost ) AS cost, SUM( conversions ) AS conversions, SUM( conversions_value ) AS conversions_value FROM products_history WHERE product_id = :product_id AND campaign_id = :campaign_id AND ad_group_id = :ad_group_id AND date_add <= :date_to GROUP BY date_add ORDER BY date_add DESC LIMIT 30', [ ':product_id' => $product_id, ':campaign_id' => $campaign_id, ':ad_group_id' => $ad_group_id, ':date_to' => $date_to ] ) -> fetchAll( \PDO::FETCH_ASSOC ); // Inicjalizacja tablic do przechowywania danych $offers_data = []; // Grupowanie danych wedug produktow foreach ( $data as $entry ) { if ( !isset( $offers_data[$product_id] ) ) { $offers_data[$product_id] = [ 'impressions' => 0, 'clicks' => 0, 'cost' => 0.0, 'conversions' => 0, 'conversions_value' => 0.0, 'roas' => 0, 'days_counted' => [] ]; } // Sumowanie danych wedug produktu $offers_data[$product_id]['impressions'] += $entry['impressions']; $offers_data[$product_id]['clicks'] += $entry['clicks']; $offers_data[$product_id]['cost'] += $entry['cost']; $offers_data[$product_id]['conversions'] += $entry['conversions']; $offers_data[$product_id]['conversions_value'] += $entry['conversions_value']; $offers_data[$product_id]['days_counted'][] = $entry['date_add']; } foreach ( $offers_data as $offer ) { $day_count = count( $offer['days_counted'] ); $impressions = $offer['impressions']; $clicks = $offer['clicks']; $ctr = ( $clicks > 0 and $impressions ) ? round( $clicks / $impressions, 4 ) * 100 : 0; $cost = $offer['cost']; $conversions = $offer['conversions']; $conversions_value = $offer['conversions_value']; $roas = ( $conversions_value > 0 and $cost ) ? round( $conversions_value / $cost, 2 ) * 100 : 0; $days_count_for_product = (int) $mdb -> query( 'SELECT COUNT( DISTINCT date_add ) FROM products_history WHERE product_id = :product_id AND campaign_id = :campaign_id AND ad_group_id = :ad_group_id AND date_add <= :date_to', [ ':product_id' => $product_id, ':campaign_id' => $campaign_id, ':ad_group_id' => $ad_group_id, ':date_to' => $date_to ] ) -> fetchColumn(); if ( $days_count_for_product >= 14 ) { if ( $mdb -> count( 'products_history_30', [ 'AND' => [ 'product_id' => $product_id, 'campaign_id' => $campaign_id, 'ad_group_id' => $ad_group_id, 'date_add' => $date_to ] ] ) > 0 ) { $mdb -> update( 'products_history_30', [ 'impressions' => $impressions, 'clicks' => $clicks, 'ctr' => $ctr, 'cost' => $cost, 'conversions' => $conversions, 'conversions_value' => $conversions_value, 'roas' => $roas, 'roas_all_time' => self::get_roas_all_time( $product_id, $date_to, $campaign_id, $ad_group_id ) ], [ 'AND' => [ 'product_id' => $product_id, 'campaign_id' => $campaign_id, 'ad_group_id' => $ad_group_id, 'date_add' => $date_to ] ] ); } else { $mdb -> insert( 'products_history_30', [ 'product_id' => $product_id, 'campaign_id' => $campaign_id, 'ad_group_id' => $ad_group_id, 'impressions' => $impressions, 'clicks' => $clicks, 'ctr' => $ctr, 'cost' => $cost, 'conversions' => $conversions, 'conversions_value' => $conversions_value, 'roas' => $roas, 'roas_all_time' => self::get_roas_all_time( $product_id, $date_to, $campaign_id, $ad_group_id ), 'date_add' => $date_to ] ); } } } } static public function cron_xml() { $result = self::generate_custom_feed_for_client( \S::get( 'client_id' ), true ); if ( ( $result['status'] ?? '' ) !== 'ok' ) { $response = [ 'result' => $result['message'] ?? 'Nie udalo sie wygenerowac pliku XML.' ]; if ( !empty( $result['client'] ) ) { $response['client'] = $result['client']; } echo json_encode( $response ); exit; } $url = (string) ( $result['url'] ?? '' ); echo json_encode( [ 'result' => 'Plik XML zostal wygenerowany ' . $url . '.' ] ); exit; } static public function generate_custom_feed_for_client( $client_id, $touch_invocation = true ) { global $mdb; $client_id = (int) $client_id; if ( $touch_invocation ) { self::touch_cron_invocation( 'cron_xml' ); } if ( $client_id <= 0 ) { return [ 'status' => 'error', 'message' => 'Nie podano ID klienta.' ]; } if ( !$mdb -> count( 'clients', [ 'id' => $client_id ] ) ) { return [ 'status' => 'error', 'message' => 'Nie znaleziono klienta o podanym ID.', 'client' => 'Nie istnieje' ]; } $results = $mdb -> query( 'SELECT * FROM products AS p INNER JOIN products_data AS pd ON p.id = pd.product_id WHERE p.client_id = ' . $client_id ) -> fetchAll( \PDO::FETCH_ASSOC ); if ( empty( $results ) ) { return [ 'status' => 'error', 'message' => 'Brak produktow do wygenerowania pliku XML.' ]; } $doc = new \DOMDocument( '1.0', 'UTF-8' ); $xmlRoot = $doc -> createElement( 'rss' ); $xmlRoot = $doc -> appendChild( $xmlRoot ); $xmlRoot -> setAttribute( 'version', '2.0' ); $xmlRoot -> setAttributeNS( 'http://www.w3.org/2000/xmlns/', 'xmlns:g', 'http://base.google.com/ns/1.0' ); $channelNode = $xmlRoot -> appendChild( $doc -> createElement( 'channel' ) ); $channelNode -> appendChild( $doc -> createElement( 'title', 'Custom Feed' ) ); $channelNode -> appendChild( $doc -> createElement( 'link', 'https://ads.pagedev.pl' ) ); $fieldMappings = [ 'title' => 'g:title', 'description' => 'g:description', 'custom_label_4' => 'g:custom_label_4', 'custom_label_3' => 'g:custom_label_3', 'google_product_category' => 'g:google_product_category' ]; foreach ( $results as $row ) { $hasValidField = false; foreach ( $fieldMappings as $dbField => $xmlTag ) { if ( !empty( $row[ $dbField ] ) ) { $hasValidField = true; break; } } if ( !$hasValidField ) { continue; } $itemNode = $channelNode -> appendChild( $doc -> createElement( 'item' ) ); $offer_id = $mdb -> get( 'products', 'offer_id', [ 'id' => $row['product_id'] ] ); $offer_id = str_replace( 'shopify_pl', 'shopify_PL', $offer_id ); $itemNode -> appendChild( $doc -> createElement( 'id', $offer_id ) ); foreach ( $fieldMappings as $dbField => $xmlTag ) { if ( !empty( $row[ $dbField ] ) ) { $itemNode -> appendChild( $doc -> createElement( $xmlTag, $row[ $dbField ] ) ); } } } $xml_dir = dirname( __DIR__, 2 ) . DIRECTORY_SEPARATOR . 'xml'; if ( !is_dir( $xml_dir ) ) { @mkdir( $xml_dir, 0777, true ); } $file_path = $xml_dir . DIRECTORY_SEPARATOR . 'custom-feed-' . $client_id . '.xml'; $save_result = @file_put_contents( $file_path, $doc -> saveXML() ); if ( $save_result === false ) { return [ 'status' => 'error', 'message' => 'Nie udalo sie zapisac pliku XML na serwerze.' ]; } $scheme = ( !empty( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off' ) ? 'https' : 'http'; $host = $_SERVER['HTTP_HOST'] ?? ( $_SERVER['SERVER_NAME'] ?? 'localhost' ); $url = $scheme . '://' . $host . '/xml/custom-feed-' . $client_id . '.xml'; return [ 'status' => 'ok', 'message' => 'Plik XML zostal wygenerowany.', 'url' => $url, 'client_id' => $client_id ]; } static public function cron_phrases() { global $mdb; self::touch_cron_invocation( __FUNCTION__ ); if ( !$client_id = \S::get( 'client_id' ) ) { echo json_encode( [ 'result' => "Nie podano ID klienta." ] ); exit; } if ( !$mdb -> count( 'clients', [ 'id' => $client_id ] ) ) { echo json_encode( [ 'result' => "Nie znaleziono klienta o podanym ID.", "client" => "Nie istnieje" ] ); exit; } $data = $mdb -> query( 'SELECT * FROM phrases AS p INNER JOIN phrases_history AS ph ON p.id = ph.phrase_id WHERE p.client_id = ' . $client_id ) -> fetchAll( \PDO::FETCH_ASSOC ); $aggregated_data = []; foreach ( $data as $row ) { $phrase_id = $row['phrase_id']; if ( !isset( $aggregated_data[$client_id] ) ) { $aggregated_data[$client_id] = []; } if ( !isset( $aggregated_data[$client_id][$phrase_id] ) ) { $aggregated_data[$client_id][$phrase_id] = [ 'phrase_id' => $phrase_id, 'phrase' => $row['phrase'], 'impressions' => 0, 'clicks' => 0, 'cost' => 0.0, 'conversions' => 0, 'conversions_value' => 0.0 ]; } $aggregated_data[$client_id][$phrase_id]['impressions'] += $row['impressions']; $aggregated_data[$client_id][$phrase_id]['clicks'] += $row['clicks']; $aggregated_data[$client_id][$phrase_id]['cost'] += $row['cost']; $aggregated_data[$client_id][$phrase_id]['conversions'] += $row['conversions']; $aggregated_data[$client_id][$phrase_id]['conversions_value'] += $row['conversions_value']; } $phrases_ids = $mdb -> select( 'phrases', 'id', [ 'client_id' => $client_id ] ); foreach ( $phrases_ids as $phrase_id ) { $phrases_ids_array[] = $phrase_id -> id; } $mdb -> delete( 'phrases_temp', [ 'phrase_id' => $phrases_ids_array ] ); foreach ( $aggregated_data as $client_phrases ) { foreach ( $client_phrases as $phrase_data ) { $cpc = $phrase_data['clicks'] > 0 ? round( $phrase_data['cost'] / $phrase_data['clicks'], 6 ) : 0; $roas = ( $phrase_data['conversions'] > 0 and $phrase_data['cost'] ) ? round( $phrase_data['conversions_value'] / $phrase_data['cost'], 2 ) * 100 : 0; $mdb -> insert( 'phrases_temp', [ 'phrase_id' => $phrase_data['phrase_id'], 'phrase' => $phrase_data['phrase'], 'impressions' => $phrase_data['impressions'], 'clicks' => $phrase_data['clicks'], 'cost' => $phrase_data['cost'], 'conversions' => $phrase_data['conversions'], 'conversions_value' => $phrase_data['conversions_value'], 'cpc' => $cpc, 'roas' => $roas, ] ); } } echo json_encode( [ 'result' => "Agregacja zakonczona, dane zapisane do phrases_temp." ] ); exit; } static public function cron_phrases_history_30() { global $mdb; self::touch_cron_invocation( __FUNCTION__ ); $start_time = microtime( true ); // Rozpoczcie mierzenia czasu $client_id = \S::get( 'client_id' ); // Pobranie ID klienta if ( !$client_id ) // Jeli nie podano ID klienta { echo json_encode( [ 'result' => "Nie podano ID klienta." ] ); // Wyswietlenie komunikatu exit; // Zakonczenie dziaania skryptu } if ( !$mdb -> count( 'clients', [ 'id' => $client_id ] ) ) // Sprawdzenie, czy klient istnieje { echo json_encode( [ 'result' => "Nie znaleziono klienta o podanym ID.", "client" => "Nie istnieje" ] ); // Wyswietlenie komunikatu exit; // Zakonczenie dziaania skryptu } // Pobranie biecej daty i daty sprzed 30 dni $phrases = $mdb -> query( 'SELECT * FROM phrases WHERE client_id = ' . $client_id ) -> fetchAll( \PDO::FETCH_ASSOC ); // Pobranie fraz dla danego klienta foreach ( $phrases as $phrase ) { for ( $i = 0; $i < 30; $i++ ) { $date_to = date( 'Y-m-d', strtotime( '-' . ( 1 + $i ) . ' days' ) ); $date_from = date( 'Y-m-d', strtotime( '-' . ( 31 + $i ) . ' days' ) ); $data_updated = false; if ( $mdb -> count( 'phrases_history', [ 'AND' => [ 'phrase_id' => $phrase['id'], 'date_add[>=]' => $date_from, 'date_add[<=]' => $date_to, 'updated' => 1 ] ] ) > 0 ) { $data_updated = true; } if ( $data_updated ) { self::cron_phrase_history_30_save( $phrase['id'], $date_from, $date_to ); } } $mdb -> update( 'phrases_history', [ 'updated' => 0 ], [ 'AND' => [ 'phrase_id' => $phrase['id'], 'updated' => 1 ] ] ); } $end_time = microtime( true ); // Zakonczenie mierzenia czasu $execution_time = $end_time - $start_time; // Obliczenie czasu wykonania echo json_encode( [ 'result' => "Agregacja zakonczona, dane zapisane do phrases_history_30. Czas wykonania skryptu: " . round( $execution_time, 4 ) . " sekund.", 'client' => \factory\Campaigns::get_client_name( $client_id ) ] ); // Wyswietlenie komunikatu exit; } // =========================== // KAMPANIE - Google Ads API // =========================== static public function cron_campaigns() { global $mdb, $settings; self::touch_cron_invocation( __FUNCTION__ ); $api = new \services\GoogleAdsApi(); if ( !$api -> is_configured() ) { echo json_encode( [ 'result' => 'Google Ads API nie jest skonfigurowane. Uzupelnij dane w Ustawieniach.' ] ); 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 ) { $client = $mdb -> get( 'clients', '*', [ 'AND' => [ 'id' => $client_id, 'google_ads_customer_id[!]' => null, 'deleted' => 0 ] ] ); if ( !$client ) { echo json_encode( [ 'result' => 'Nie znaleziono klienta z poprawnym Google Ads Customer ID.', 'client_id' => $client_id ] ); exit; } $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 ), 'keywords_synced' => (int) ( $sync['keywords_synced'] ?? 0 ), 'negative_keywords_synced' => (int) ( $sync['negative_keywords_synced'] ?? 0 ), 'errors' => $sync['errors'] ] ); exit; } self::cleanup_old_sync_rows( 30 ); $client_ids = $mdb -> query( "SELECT id FROM clients WHERE deleted = 0 AND google_ads_customer_id IS NOT NULL AND google_ads_customer_id <> '' ORDER BY id ASC" ) -> fetchAll( \PDO::FETCH_COLUMN ); $client_ids = array_values( array_unique( array_map( 'intval', $client_ids ) ) ); if ( empty( $client_ids ) ) { echo json_encode( [ 'result' => 'Brak klientow z ustawionym Google Ads Customer ID.' ] ); exit; } $clients_map = []; $clients = $mdb -> select( 'clients', '*', [ 'AND' => [ 'google_ads_customer_id[!]' => null, 'deleted' => 0 ], 'ORDER' => [ 'id' => 'ASC' ] ] ); foreach ( $clients as $c ) { $clients_map[ (int) $c['id'] ] = $c; } self::ensure_sync_rows( 'campaigns', $sync_dates, $client_ids ); $active_client_id = self::get_active_client( 'campaigns' ); if ( !$active_client_id ) { echo json_encode( [ 'result' => 'Wszyscy klienci kampanii zostali juz przetworzeni dla calego okna dat.', 'date' => $sync_date, 'active_date' => $sync_date, 'conversion_window_days' => $conversion_window_days, 'dates_synced' => $sync_dates, 'processed_clients' => count( $client_ids ), 'total_clients' => count( $client_ids ) ] ); exit; } $dates_per_run_default = (int) ( $settings['cron_campaigns_clients_per_run'] ?? 2 ); if ( $dates_per_run_default <= 0 ) { $dates_per_run_default = 2; } $dates_per_run = (int) \S::get( 'clients_per_run' ); if ( $dates_per_run <= 0 ) { $dates_per_run = $dates_per_run_default; } $dates_per_run = min( 20, $dates_per_run ); $dates_batch = self::get_pending_dates_for_client( 'campaigns', $active_client_id, 'pending', $dates_per_run ); if ( empty( $dates_batch ) ) { echo json_encode( [ 'result' => 'Wszystkie daty klienta przetworzone. Kolejne wywolanie przejdzie do nastepnego klienta.', 'date' => $sync_date, 'active_client_id' => $active_client_id, 'conversion_window_days' => $conversion_window_days, 'dates_synced' => $sync_dates, 'processed_clients' => count( $client_ids ), 'total_clients' => count( $client_ids ) ] ); exit; } $selected_client = $clients_map[ $active_client_id ] ?? null; if ( !$selected_client ) { echo json_encode( [ 'result' => 'Nie udalo sie znalezc klienta do synchronizacji kampanii. ID: ' . $active_client_id, 'active_client_id' => $active_client_id, 'errors' => [ 'Klient ID ' . $active_client_id . ' nie znaleziony w clients_map.' ] ] ); exit; } $dates_processed_in_call = []; $errors = []; $processed_records_total = 0; $ad_groups_synced_total = 0; $search_terms_synced_total = 0; $keywords_synced_total = 0; $negative_keywords_synced_total = 0; foreach ( $dates_batch as $active_date ) { $sync_details = ( $active_date === $sync_date ); $sync = self::sync_campaigns_for_client( $selected_client, $api, $active_date, $sync_details ); $processed_records_total += (int) ( $sync['processed_records'] ?? 0 ); $ad_groups_synced_total += (int) ( $sync['ad_groups_synced'] ?? 0 ); $search_terms_synced_total += (int) ( $sync['search_terms_synced'] ?? 0 ); $keywords_synced_total += (int) ( $sync['keywords_synced'] ?? 0 ); $negative_keywords_synced_total += (int) ( $sync['negative_keywords_synced'] ?? 0 ); $error_msg = null; if ( !empty( $sync['errors'] ) ) { $errors = array_merge( $errors, (array) $sync['errors'] ); $error_msg = implode( '; ', (array) $sync['errors'] ); } self::mark_sync_phase( 'campaigns', $active_date, $active_client_id, 'done', $error_msg ); $dates_processed_in_call[] = $active_date; } $done_count = (int) $mdb -> query( "SELECT COUNT(*) FROM cron_sync_status WHERE pipeline = 'campaigns' AND client_id = :client_id AND phase = 'done' AND sync_date >= DATE_SUB(CURDATE(), INTERVAL 90 DAY)", [ ':client_id' => $active_client_id ] ) -> fetchColumn(); $total_count = (int) $mdb -> query( "SELECT COUNT(*) FROM cron_sync_status WHERE pipeline = 'campaigns' AND client_id = :client_id AND sync_date >= DATE_SUB(CURDATE(), INTERVAL 90 DAY)", [ ':client_id' => $active_client_id ] ) -> fetchColumn(); $remaining_dates = max( 0, $total_count - $done_count ); $estimated_calls_remaining = (int) ceil( $remaining_dates / max( 1, $dates_per_run ) ); echo json_encode( [ 'result' => empty( $errors ) ? 'Synchronizacja kampanii zakonczona.' : 'Synchronizacja kampanii zakonczona z bledami.', 'active_client_id' => $active_client_id, 'dates_per_run' => $dates_per_run, 'dates_processed_in_call' => count( $dates_processed_in_call ), 'processed_dates' => $dates_processed_in_call, 'remaining_dates' => $remaining_dates, 'estimated_calls_remaining' => $estimated_calls_remaining, 'date' => $sync_date, 'conversion_window_days' => $conversion_window_days, 'dates_synced' => $sync_dates, 'processed_records' => $processed_records_total, 'ad_groups_synced' => $ad_groups_synced_total, 'search_terms_synced' => $search_terms_synced_total, 'keywords_synced' => $keywords_synced_total, 'negative_keywords_synced' => $negative_keywords_synced_total, 'total_clients' => count( $client_ids ), 'errors' => $errors ] ); exit; } static private function sync_campaigns_for_client( $client, $api, $as_of_date = null, $sync_details = true ) { global $mdb; $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, $as_of_date ); if ( $campaigns_30 === false ) { $last_err = \services\GoogleAdsApi::get_setting( 'google_ads_last_error' ); $errors[] = 'Blad API dla klienta ' . $client['name'] . ' (ID: ' . $customer_id . '): ' . $last_err; return [ 'processed_records' => 0, 'ad_groups_synced' => 0, 'search_terms_synced' => 0, 'keywords_synced' => 0, 'negative_keywords_synced' => 0, 'errors' => $errors ]; } if ( !is_array( $campaigns_30 ) ) { $campaigns_30 = []; } $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, 'keywords_synced' => 0, 'negative_keywords_synced' => 0, 'errors' => $errors ]; } $all_time_map = []; $all_time_totals = [ 'cost' => 0.0, 'conversion_value' => 0.0, ]; if ( is_array( $campaigns_all_time ) ) { foreach ( $campaigns_all_time as $cat ) { $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 ); } } $account_30_totals = [ 'budget' => 0.0, 'money_spent' => 0.0, 'conversion_value' => 0.0, ]; foreach ( $campaigns_30 as $campaign ) { $external_campaign_id = isset( $campaign['campaign_id'] ) ? (string) $campaign['campaign_id'] : ''; if ( $external_campaign_id === '' ) { continue; } $advertising_channel_type = strtoupper( trim( (string) ( $campaign['advertising_channel_type'] ?? '' ) ) ); $account_30_totals['budget'] += (float) ( $campaign['budget'] ?? 0 ); $account_30_totals['money_spent'] += (float) ( $campaign['money_spent'] ?? 0 ); $account_30_totals['conversion_value'] += (float) ( $campaign['conversion_value'] ?? 0 ); if ( !$mdb -> count( 'campaigns', [ 'AND' => [ 'client_id' => $client['id'], 'campaign_id' => $external_campaign_id ] ] ) ) { $mdb -> insert( 'campaigns', [ 'client_id' => $client['id'], 'campaign_id' => $external_campaign_id, 'campaign_name' => $campaign['campaign_name'], 'advertising_channel_type' => $advertising_channel_type !== '' ? $advertising_channel_type : null ] ); $db_campaign_id = $mdb -> id(); } else { $db_campaign_id = $mdb -> get( 'campaigns', 'id', [ 'AND' => [ 'client_id' => $client['id'], 'campaign_id' => $external_campaign_id ] ] ); $mdb -> update( 'campaigns', [ 'campaign_name' => $campaign['campaign_name'], 'advertising_channel_type' => $advertising_channel_type !== '' ? $advertising_channel_type : null ], [ 'id' => $db_campaign_id ] ); } $bidding_strategy = self::format_bidding_strategy( $campaign['bidding_strategy'], $campaign['target_roas'] ?? 0 ); $history_data = [ 'roas_30_days' => $campaign['roas_30_days'], 'roas_all_time' => $all_time_map[ $external_campaign_id ] ?? 0, 'budget' => $campaign['budget'], 'money_spent' => $campaign['money_spent'], 'conversion_value' => $campaign['conversion_value'], 'bidding_strategy' => $bidding_strategy, ]; if ( $mdb -> count( 'campaigns_history', [ 'AND' => [ 'campaign_id' => $db_campaign_id, 'date_add' => $as_of_date ] ] ) ) { $mdb -> update( 'campaigns_history', $history_data, [ 'AND' => [ 'campaign_id' => $db_campaign_id, 'date_add' => $as_of_date ] ] ); } else { $history_data['campaign_id'] = $db_campaign_id; $history_data['date_add'] = $as_of_date; $mdb -> insert( 'campaigns_history', $history_data ); } $campaigns_db_map[ $external_campaign_id ] = (int) $db_campaign_id; $processed++; } $account_roas_30 = ( $account_30_totals['money_spent'] > 0 ) ? round( ( $account_30_totals['conversion_value'] / $account_30_totals['money_spent'] ) * 100, 2 ) : 0; $account_roas_all_time = ( $all_time_totals['cost'] > 0 ) ? round( ( $all_time_totals['conversion_value'] / $all_time_totals['cost'] ) * 100, 2 ) : 0; if ( !$mdb -> count( 'campaigns', [ 'AND' => [ 'client_id' => $client['id'], 'campaign_id' => 0 ] ] ) ) { $mdb -> insert( 'campaigns', [ 'client_id' => $client['id'], 'campaign_id' => 0, 'campaign_name' => '--- konto ---', 'advertising_channel_type' => null ] ); $db_account_campaign_id = $mdb -> id(); } else { $db_account_campaign_id = $mdb -> get( 'campaigns', 'id', [ 'AND' => [ 'client_id' => $client['id'], 'campaign_id' => 0 ] ] ); $mdb -> update( 'campaigns', [ 'campaign_name' => '--- konto ---', 'advertising_channel_type' => null ], [ 'id' => $db_account_campaign_id ] ); } $account_history_data = [ 'roas_30_days' => $account_roas_30, 'roas_all_time' => $account_roas_all_time, 'budget' => $account_30_totals['budget'], 'money_spent' => $account_30_totals['money_spent'], 'conversion_value' => $account_30_totals['conversion_value'], 'bidding_strategy' => 'Konto (agregacja wszystkich kampanii)', ]; if ( $mdb -> count( 'campaigns_history', [ 'AND' => [ 'campaign_id' => $db_account_campaign_id, 'date_add' => $as_of_date ] ] ) ) { $mdb -> update( 'campaigns_history', $account_history_data, [ 'AND' => [ 'campaign_id' => $db_account_campaign_id, 'date_add' => $as_of_date ] ] ); } else { $account_history_data['campaign_id'] = $db_account_campaign_id; $account_history_data['date_add'] = $as_of_date; $mdb -> insert( 'campaigns_history', $account_history_data ); } $processed++; if ( !$sync_details ) { return [ 'processed_records' => $processed, 'ad_groups_synced' => 0, 'search_terms_synced' => 0, 'keywords_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 ); $keywords_sync = self::sync_campaign_keywords_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'], $keywords_sync['errors'], $negative_keywords_sync['errors'] ); return [ 'processed_records' => $processed, 'ad_groups_synced' => (int) $ad_groups_sync['count'], 'search_terms_synced' => (int) $search_terms_sync['count'], 'keywords_synced' => (int) $keywords_sync['count'], 'negative_keywords_synced' => (int) $negative_keywords_sync['count'], 'errors' => $errors ]; } static private function sync_campaign_ad_groups_for_client( $campaigns_db_map, $customer_id, $api, $date_sync ) { global $mdb; $campaign_db_ids = array_values( array_unique( array_map( 'intval', array_values( $campaigns_db_map ) ) ) ); if ( empty( $campaign_db_ids ) ) { return [ 'count' => 0, 'ad_group_map' => [], 'errors' => [] ]; } $ad_groups_30 = $api -> get_ad_groups_30_days( $customer_id ); if ( $ad_groups_30 === false ) { $last_err = \services\GoogleAdsApi::get_setting( 'google_ads_last_error' ); return [ 'count' => 0, 'ad_group_map' => [], 'errors' => [ 'Blad pobierania grup reklam (30 dni): ' . $last_err ] ]; } $ad_groups_all_time = $api -> get_ad_groups_all_time( $customer_id ); if ( $ad_groups_all_time === false ) { $last_err = \services\GoogleAdsApi::get_setting( 'google_ads_last_error' ); return [ 'count' => 0, 'ad_group_map' => [], 'errors' => [ 'Blad pobierania grup reklam (all time): ' . $last_err ] ]; } if ( !is_array( $ad_groups_30 ) ) { $ad_groups_30 = []; } if ( !is_array( $ad_groups_all_time ) ) { $ad_groups_all_time = []; } $map_30 = []; foreach ( $ad_groups_30 as $row ) { $campaign_external_id = isset( $row['campaign_id'] ) ? (string) $row['campaign_id'] : ''; $ad_group_external_id = isset( $row['ad_group_id'] ) ? (string) $row['ad_group_id'] : ''; if ( $campaign_external_id === '' || $ad_group_external_id === '' ) { continue; } $map_30[ $campaign_external_id . '|' . $ad_group_external_id ] = $row; } $map_all_time = []; foreach ( $ad_groups_all_time as $row ) { $campaign_external_id = isset( $row['campaign_id'] ) ? (string) $row['campaign_id'] : ''; $ad_group_external_id = isset( $row['ad_group_id'] ) ? (string) $row['ad_group_id'] : ''; if ( $campaign_external_id === '' || $ad_group_external_id === '' ) { continue; } $map_all_time[ $campaign_external_id . '|' . $ad_group_external_id ] = $row; } $mdb -> delete( 'campaign_ad_groups', [ 'campaign_id' => $campaign_db_ids ] ); $keys = array_values( array_unique( array_merge( array_keys( $map_30 ), array_keys( $map_all_time ) ) ) ); $ad_group_db_map = []; $count = 0; foreach ( $keys as $key ) { $parts = explode( '|', $key, 2 ); $campaign_external_id = $parts[0] ?? ''; $ad_group_external_id = $parts[1] ?? ''; $db_campaign_id = (int) ( $campaigns_db_map[ $campaign_external_id ] ?? 0 ); if ( $db_campaign_id <= 0 || $ad_group_external_id === '' ) { continue; } $row_30 = $map_30[ $key ] ?? []; $row_all_time = $map_all_time[ $key ] ?? []; $ad_group_name = trim( (string) ( $row_30['ad_group_name'] ?? ( $row_all_time['ad_group_name'] ?? '' ) ) ); if ( $ad_group_name === '' ) { $ad_group_name = 'Ad group #' . $ad_group_external_id; } $mdb -> insert( 'campaign_ad_groups', [ 'campaign_id' => $db_campaign_id, 'ad_group_id' => (int) $ad_group_external_id, 'ad_group_name' => $ad_group_name, 'impressions_30' => (int) ( $row_30['impressions'] ?? 0 ), 'clicks_30' => (int) ( $row_30['clicks'] ?? 0 ), 'cost_30' => (float) ( $row_30['cost'] ?? 0 ), 'conversions_30' => (float) ( $row_30['conversions'] ?? 0 ), 'conversion_value_30' => (float) ( $row_30['conversion_value'] ?? 0 ), 'roas_30' => (float) ( $row_30['roas'] ?? 0 ), 'impressions_all_time' => (int) ( $row_all_time['impressions'] ?? 0 ), 'clicks_all_time' => (int) ( $row_all_time['clicks'] ?? 0 ), 'cost_all_time' => (float) ( $row_all_time['cost'] ?? 0 ), 'conversions_all_time' => (float) ( $row_all_time['conversions'] ?? 0 ), 'conversion_value_all_time' => (float) ( $row_all_time['conversion_value'] ?? 0 ), 'roas_all_time' => (float) ( $row_all_time['roas'] ?? 0 ), 'date_sync' => $date_sync ] ); $db_ad_group_id = (int) $mdb -> id(); if ( $db_ad_group_id > 0 ) { $ad_group_db_map[ $key ] = $db_ad_group_id; $count++; } } return [ 'count' => $count, 'ad_group_map' => $ad_group_db_map, 'errors' => [] ]; } static private function sync_campaign_search_terms_for_client( $campaigns_db_map, $ad_group_db_map, $customer_id, $api, $date_sync ) { global $mdb; $campaign_db_ids = array_values( array_unique( array_map( 'intval', array_values( $campaigns_db_map ) ) ) ); if ( empty( $campaign_db_ids ) ) { return [ 'count' => 0, 'errors' => [] ]; } $search_terms_30 = $api -> get_search_terms_30_days( $customer_id ); if ( $search_terms_30 === false ) { $last_err = \services\GoogleAdsApi::get_setting( 'google_ads_last_error' ); return [ 'count' => 0, 'errors' => [ 'Blad pobierania fraz wyszukiwanych (30 dni): ' . $last_err ] ]; } $search_terms_all_time = $api -> get_search_terms_all_time( $customer_id ); if ( $search_terms_all_time === false ) { $last_err = \services\GoogleAdsApi::get_setting( 'google_ads_last_error' ); return [ 'count' => 0, 'errors' => [ 'Blad pobierania fraz wyszukiwanych (all time): ' . $last_err ] ]; } if ( !is_array( $search_terms_30 ) ) { $search_terms_30 = []; } if ( !is_array( $search_terms_all_time ) ) { $search_terms_all_time = []; } $map_30 = []; foreach ( $search_terms_30 as $row ) { $campaign_external_id = isset( $row['campaign_id'] ) ? (string) $row['campaign_id'] : ''; $ad_group_external_id = isset( $row['ad_group_id'] ) ? (string) $row['ad_group_id'] : ''; $search_term = trim( (string) ( $row['search_term'] ?? '' ) ); if ( $campaign_external_id === '' || $ad_group_external_id === '' || $search_term === '' ) { continue; } $map_30[ $campaign_external_id . '|' . $ad_group_external_id . '|' . strtolower( $search_term ) ] = $row; } $map_all_time = []; foreach ( $search_terms_all_time as $row ) { $campaign_external_id = isset( $row['campaign_id'] ) ? (string) $row['campaign_id'] : ''; $ad_group_external_id = isset( $row['ad_group_id'] ) ? (string) $row['ad_group_id'] : ''; $search_term = trim( (string) ( $row['search_term'] ?? '' ) ); if ( $campaign_external_id === '' || $ad_group_external_id === '' || $search_term === '' ) { continue; } $map_all_time[ $campaign_external_id . '|' . $ad_group_external_id . '|' . strtolower( $search_term ) ] = $row; } $mdb -> delete( 'campaign_search_terms', [ 'campaign_id' => $campaign_db_ids ] ); $keys = array_values( array_unique( array_merge( array_keys( $map_30 ), array_keys( $map_all_time ) ) ) ); $count = 0; foreach ( $keys as $key ) { $parts = explode( '|', $key, 3 ); $campaign_external_id = $parts[0] ?? ''; $ad_group_external_id = $parts[1] ?? ''; $db_campaign_id = (int) ( $campaigns_db_map[ $campaign_external_id ] ?? 0 ); $db_ad_group_id = (int) ( $ad_group_db_map[ $campaign_external_id . '|' . $ad_group_external_id ] ?? 0 ); if ( $db_campaign_id > 0 && $db_ad_group_id <= 0 && $ad_group_external_id === '0' ) { $db_ad_group_id = self::ensure_campaign_level_ad_group( $db_campaign_id, $date_sync ); if ( $db_ad_group_id > 0 ) { $ad_group_db_map[ $campaign_external_id . '|0' ] = $db_ad_group_id; } } if ( $db_campaign_id <= 0 || $db_ad_group_id <= 0 ) { continue; } $row_30 = $map_30[ $key ] ?? []; $row_all_time = $map_all_time[ $key ] ?? []; $search_term = trim( (string) ( $row_30['search_term'] ?? ( $row_all_time['search_term'] ?? '' ) ) ); if ( $search_term === '' ) { continue; } $clicks_30 = (int) ( $row_30['clicks'] ?? 0 ); $clicks_all_time = (int) ( $row_all_time['clicks'] ?? 0 ); if ( $clicks_30 <= 0 && $clicks_all_time <= 0 ) { continue; } $mdb -> insert( 'campaign_search_terms', [ 'campaign_id' => $db_campaign_id, 'ad_group_id' => $db_ad_group_id, 'search_term' => $search_term, 'impressions_30' => (int) ( $row_30['impressions'] ?? 0 ), 'clicks_30' => $clicks_30, 'cost_30' => (float) ( $row_30['cost'] ?? 0 ), 'conversions_30' => (float) ( $row_30['conversions'] ?? 0 ), 'conversion_value_30' => (float) ( $row_30['conversion_value'] ?? 0 ), 'roas_30' => (float) ( $row_30['roas'] ?? 0 ), 'impressions_all_time' => (int) ( $row_all_time['impressions'] ?? 0 ), 'clicks_all_time' => $clicks_all_time, 'cost_all_time' => (float) ( $row_all_time['cost'] ?? 0 ), 'conversions_all_time' => (float) ( $row_all_time['conversions'] ?? 0 ), 'conversion_value_all_time' => (float) ( $row_all_time['conversion_value'] ?? 0 ), 'roas_all_time' => (float) ( $row_all_time['roas'] ?? 0 ), 'date_sync' => $date_sync ] ); $count++; } return [ 'count' => $count, 'errors' => [] ]; } static private function ensure_campaign_level_ad_group( $db_campaign_id, $date_sync ) { global $mdb; $db_campaign_id = (int) $db_campaign_id; if ( $db_campaign_id <= 0 ) { return 0; } $existing_id = (int) $mdb -> get( 'campaign_ad_groups', 'id', [ 'AND' => [ 'campaign_id' => $db_campaign_id, 'ad_group_id' => 0 ] ] ); if ( $existing_id > 0 ) { return $existing_id; } $mdb -> insert( 'campaign_ad_groups', [ 'campaign_id' => $db_campaign_id, 'ad_group_id' => 0, 'ad_group_name' => 'PMax (bez grup reklam)', 'impressions_30' => 0, 'clicks_30' => 0, 'cost_30' => 0, 'conversions_30' => 0, 'conversion_value_30' => 0, 'roas_30' => 0, 'impressions_all_time' => 0, 'clicks_all_time' => 0, 'cost_all_time' => 0, 'conversions_all_time' => 0, 'conversion_value_all_time' => 0, 'roas_all_time' => 0, 'date_sync' => $date_sync ] ); return (int) $mdb -> id(); } static private function sync_campaign_keywords_for_client( $campaigns_db_map, $ad_group_db_map, $customer_id, $api, $date_sync ) { global $mdb; $campaign_db_ids = array_values( array_unique( array_map( 'intval', array_values( $campaigns_db_map ) ) ) ); if ( empty( $campaign_db_ids ) ) { return [ 'count' => 0, 'errors' => [] ]; } $keywords_30 = $api -> get_ad_keywords_30_days( $customer_id ); if ( $keywords_30 === false ) { $last_err = \services\GoogleAdsApi::get_setting( 'google_ads_last_error' ); return [ 'count' => 0, 'errors' => [ 'Blad pobierania slow kluczowych (30 dni): ' . $last_err ] ]; } $keywords_all_time = $api -> get_ad_keywords_all_time( $customer_id ); if ( $keywords_all_time === false ) { $last_err = \services\GoogleAdsApi::get_setting( 'google_ads_last_error' ); return [ 'count' => 0, 'errors' => [ 'Blad pobierania slow kluczowych (all time): ' . $last_err ] ]; } if ( !is_array( $keywords_30 ) ) { $keywords_30 = []; } if ( !is_array( $keywords_all_time ) ) { $keywords_all_time = []; } $map_30 = []; foreach ( $keywords_30 as $row ) { $campaign_external_id = isset( $row['campaign_id'] ) ? (string) $row['campaign_id'] : ''; $ad_group_external_id = isset( $row['ad_group_id'] ) ? (string) $row['ad_group_id'] : ''; $keyword_text = trim( (string) ( $row['keyword_text'] ?? '' ) ); $match_type = trim( (string) ( $row['match_type'] ?? '' ) ); if ( $campaign_external_id === '' || $ad_group_external_id === '' || $keyword_text === '' ) { continue; } $map_30[ $campaign_external_id . '|' . $ad_group_external_id . '|' . strtolower( $keyword_text ) . '|' . strtolower( $match_type ) ] = $row; } $map_all_time = []; foreach ( $keywords_all_time as $row ) { $campaign_external_id = isset( $row['campaign_id'] ) ? (string) $row['campaign_id'] : ''; $ad_group_external_id = isset( $row['ad_group_id'] ) ? (string) $row['ad_group_id'] : ''; $keyword_text = trim( (string) ( $row['keyword_text'] ?? '' ) ); $match_type = trim( (string) ( $row['match_type'] ?? '' ) ); if ( $campaign_external_id === '' || $ad_group_external_id === '' || $keyword_text === '' ) { continue; } $map_all_time[ $campaign_external_id . '|' . $ad_group_external_id . '|' . strtolower( $keyword_text ) . '|' . strtolower( $match_type ) ] = $row; } $mdb -> delete( 'campaign_keywords', [ 'campaign_id' => $campaign_db_ids ] ); $keys = array_values( array_unique( array_merge( array_keys( $map_30 ), array_keys( $map_all_time ) ) ) ); $count = 0; foreach ( $keys as $key ) { $parts = explode( '|', $key, 4 ); $campaign_external_id = $parts[0] ?? ''; $ad_group_external_id = $parts[1] ?? ''; $db_campaign_id = (int) ( $campaigns_db_map[ $campaign_external_id ] ?? 0 ); $db_ad_group_id = (int) ( $ad_group_db_map[ $campaign_external_id . '|' . $ad_group_external_id ] ?? 0 ); if ( $db_campaign_id <= 0 || $db_ad_group_id <= 0 ) { continue; } $row_30 = $map_30[ $key ] ?? []; $row_all_time = $map_all_time[ $key ] ?? []; $keyword_text = trim( (string) ( $row_30['keyword_text'] ?? ( $row_all_time['keyword_text'] ?? '' ) ) ); if ( $keyword_text === '' ) { continue; } $match_type = trim( (string) ( $row_30['match_type'] ?? ( $row_all_time['match_type'] ?? '' ) ) ); $clicks_30 = (int) ( $row_30['clicks'] ?? 0 ); $clicks_all_time = (int) ( $row_all_time['clicks'] ?? 0 ); if ( $clicks_30 <= 0 && $clicks_all_time <= 0 ) { continue; } $mdb -> insert( 'campaign_keywords', [ 'campaign_id' => $db_campaign_id, 'ad_group_id' => $db_ad_group_id, 'keyword_text' => $keyword_text, 'match_type' => $match_type, 'impressions_30' => (int) ( $row_30['impressions'] ?? 0 ), 'clicks_30' => $clicks_30, 'cost_30' => (float) ( $row_30['cost'] ?? 0 ), 'conversions_30' => (float) ( $row_30['conversions'] ?? 0 ), 'conversion_value_30' => (float) ( $row_30['conversion_value'] ?? 0 ), 'roas_30' => (float) ( $row_30['roas'] ?? 0 ), 'impressions_all_time' => (int) ( $row_all_time['impressions'] ?? 0 ), 'clicks_all_time' => $clicks_all_time, 'cost_all_time' => (float) ( $row_all_time['cost'] ?? 0 ), 'conversions_all_time' => (float) ( $row_all_time['conversions'] ?? 0 ), 'conversion_value_all_time' => (float) ( $row_all_time['conversion_value'] ?? 0 ), 'roas_all_time' => (float) ( $row_all_time['roas'] ?? 0 ), 'date_sync' => $date_sync ] ); $count++; } return [ 'count' => $count, 'errors' => [] ]; } static private function sync_campaign_negative_keywords_for_client( $campaigns_db_map, $ad_group_db_map, $customer_id, $api, $date_sync ) { global $mdb; $campaign_db_ids = array_values( array_unique( array_map( 'intval', array_values( $campaigns_db_map ) ) ) ); if ( empty( $campaign_db_ids ) ) { return [ 'count' => 0, 'errors' => [] ]; } $negatives = $api -> get_negative_keywords( $customer_id ); if ( $negatives === false ) { $last_err = \services\GoogleAdsApi::get_setting( 'google_ads_last_error' ); return [ 'count' => 0, 'errors' => [ 'Blad pobierania fraz wykluczajacych: ' . $last_err ] ]; } if ( !is_array( $negatives ) ) { $negatives = []; } $mdb -> delete( 'campaign_negative_keywords', [ 'campaign_id' => $campaign_db_ids ] ); $count = 0; $seen = []; foreach ( $negatives as $row ) { $campaign_external_id = isset( $row['campaign_id'] ) ? (string) $row['campaign_id'] : ''; $db_campaign_id = (int) ( $campaigns_db_map[ $campaign_external_id ] ?? 0 ); if ( $db_campaign_id <= 0 ) { continue; } $scope = ( $row['scope'] ?? '' ) === 'ad_group' ? 'ad_group' : 'campaign'; $db_ad_group_id = null; if ( $scope === 'ad_group' ) { $ad_group_external_id = isset( $row['ad_group_id'] ) ? (string) $row['ad_group_id'] : ''; $mapped_ad_group_id = (int) ( $ad_group_db_map[ $campaign_external_id . '|' . $ad_group_external_id ] ?? 0 ); if ( $mapped_ad_group_id <= 0 ) { continue; } $db_ad_group_id = $mapped_ad_group_id; } $keyword_text = trim( (string) ( $row['keyword_text'] ?? '' ) ); if ( $keyword_text === '' ) { continue; } $match_type = trim( (string) ( $row['match_type'] ?? '' ) ); $uniq_key = $db_campaign_id . '|' . (int) $db_ad_group_id . '|' . $scope . '|' . strtolower( $keyword_text ) . '|' . strtolower( $match_type ); if ( isset( $seen[ $uniq_key ] ) ) { continue; } $seen[ $uniq_key ] = true; $mdb -> insert( 'campaign_negative_keywords', [ 'campaign_id' => $db_campaign_id, 'ad_group_id' => $db_ad_group_id, 'scope' => $scope, 'keyword_text' => $keyword_text, 'match_type' => $match_type, 'date_sync' => $date_sync ] ); $count++; } return [ 'count' => $count, 'errors' => [] ]; } static private function pick_clients_batch_by_cursor( $clients, $limit, $cursor_client_id = 0 ) { $clients = is_array( $clients ) ? array_values( $clients ) : []; if ( empty( $clients ) ) { return []; } $limit = max( 1, (int) $limit ); $total = count( $clients ); if ( $limit >= $total ) { return $clients; } $start_index = 0; $cursor_client_id = (int) $cursor_client_id; if ( $cursor_client_id > 0 ) { $found_next = false; foreach ( $clients as $idx => $client ) { $current_id = (int) ( $client['id'] ?? 0 ); if ( $current_id > $cursor_client_id ) { $start_index = $idx; $found_next = true; break; } } if ( !$found_next ) { $start_index = 0; } } $batch = []; for ( $i = 0; $i < $limit; $i++ ) { $batch[] = $clients[( $start_index + $i ) % $total]; } return $batch; } // =========================== // CRON SYNC STATUS HELPERS // =========================== static private function ensure_sync_rows( $pipeline, $sync_dates, $client_ids ) { global $mdb; if ( empty( $client_ids ) || empty( $sync_dates ) ) { return; } $stmt = $mdb -> pdo -> prepare( "INSERT INTO cron_sync_status (client_id, pipeline, sync_date, phase) VALUES (:client_id, :pipeline, :sync_date, 'pending') ON DUPLICATE KEY UPDATE phase = IF(phase = 'done' AND completed_at < CURDATE(), 'pending', phase), started_at = IF(phase = 'done' AND completed_at < CURDATE(), NULL, started_at), completed_at = IF(phase = 'done' AND completed_at < CURDATE(), NULL, completed_at), error_message = IF(phase = 'done' AND completed_at < CURDATE(), NULL, error_message)" ); foreach ( $sync_dates as $date ) { foreach ( $client_ids as $cid ) { $stmt -> execute( [ ':client_id' => (int) $cid, ':pipeline' => $pipeline, ':sync_date' => $date ] ); } } } static private function get_active_sync_date( $pipeline ) { global $mdb; $row = $mdb -> query( "SELECT sync_date FROM cron_sync_status WHERE pipeline = :pipeline AND phase != 'done' AND sync_date >= DATE_SUB(CURDATE(), INTERVAL 90 DAY) ORDER BY sync_date ASC LIMIT 1", [ ':pipeline' => $pipeline ] ) -> fetch( \PDO::FETCH_ASSOC ); return $row ? $row['sync_date'] : null; } static private function get_pending_clients( $pipeline, $sync_date, $source_phase, $limit ) { global $mdb; $rows = $mdb -> query( "SELECT cs.client_id FROM cron_sync_status cs INNER JOIN clients c ON cs.client_id = c.id AND c.deleted = 0 WHERE cs.pipeline = :pipeline AND cs.sync_date = :sync_date AND cs.phase = :phase ORDER BY cs.client_id ASC LIMIT " . (int) $limit, [ ':pipeline' => $pipeline, ':sync_date' => $sync_date, ':phase' => $source_phase ] ) -> fetchAll( \PDO::FETCH_COLUMN ); return is_array( $rows ) ? array_map( 'intval', $rows ) : []; } static private function mark_sync_phase( $pipeline, $sync_date, $client_id, $new_phase, $error_msg = null ) { global $mdb; $update_data = [ 'phase' => $new_phase ]; if ( $new_phase === 'done' ) { $update_data['completed_at'] = date( 'Y-m-d H:i:s' ); } else if ( $new_phase !== 'pending' ) { $update_data['started_at'] = date( 'Y-m-d H:i:s' ); } if ( $error_msg !== null ) { $update_data['error_message'] = $error_msg; } $mdb -> update( 'cron_sync_status', $update_data, [ 'AND' => [ 'client_id' => (int) $client_id, 'pipeline' => $pipeline, 'sync_date' => $sync_date ] ] ); } static private function determine_products_phase( $sync_date ) { global $mdb; $phases_order = [ 'pending', 'fetch', 'aggregate_30' ]; foreach ( $phases_order as $phase ) { $count = (int) $mdb -> query( "SELECT COUNT(*) FROM cron_sync_status cs INNER JOIN clients c ON cs.client_id = c.id AND c.deleted = 0 WHERE cs.pipeline = 'products' AND cs.sync_date = :sync_date AND cs.phase = :phase", [ ':sync_date' => $sync_date, ':phase' => $phase ] ) -> fetchColumn(); if ( $count > 0 ) { return $phase; } } return null; } static private function get_active_client( $pipeline ) { global $mdb; $row = $mdb -> query( "SELECT DISTINCT cs.client_id FROM cron_sync_status cs INNER JOIN clients c ON cs.client_id = c.id AND c.deleted = 0 WHERE cs.pipeline = :pipeline AND cs.phase != 'done' AND cs.sync_date >= DATE_SUB(CURDATE(), INTERVAL 90 DAY) ORDER BY cs.client_id ASC LIMIT 1", [ ':pipeline' => $pipeline ] ) -> fetch( \PDO::FETCH_ASSOC ); return $row ? (int) $row['client_id'] : null; } static private function get_pending_dates_for_client( $pipeline, $client_id, $phase, $limit ) { global $mdb; $rows = $mdb -> query( "SELECT cs.sync_date FROM cron_sync_status cs WHERE cs.pipeline = :pipeline AND cs.client_id = :client_id AND cs.phase = :phase AND cs.sync_date >= DATE_SUB(CURDATE(), INTERVAL 90 DAY) ORDER BY cs.sync_date ASC LIMIT " . (int) $limit, [ ':pipeline' => $pipeline, ':client_id' => (int) $client_id, ':phase' => $phase ] ) -> fetchAll( \PDO::FETCH_COLUMN ); return is_array( $rows ) ? $rows : []; } static private function determine_client_products_phase( $client_id ) { global $mdb; $phases_order = [ 'pending', 'fetch', 'aggregate_30' ]; foreach ( $phases_order as $phase ) { $count = (int) $mdb -> query( "SELECT COUNT(*) FROM cron_sync_status cs INNER JOIN clients c ON cs.client_id = c.id AND c.deleted = 0 WHERE cs.pipeline = 'products' AND cs.client_id = :client_id AND cs.phase = :phase AND cs.sync_date >= DATE_SUB(CURDATE(), INTERVAL 90 DAY)", [ ':client_id' => (int) $client_id, ':phase' => $phase ] ) -> fetchColumn(); if ( $count > 0 ) { return $phase; } } return null; } static private function cleanup_old_sync_rows( $days = 30 ) { global $mdb; $mdb -> query( "DELETE FROM cron_sync_status WHERE phase = 'done' AND completed_at < DATE_SUB(NOW(), INTERVAL :days DAY)", [ ':days' => (int) $days ] ); $mdb -> query( "DELETE cs FROM cron_sync_status cs LEFT JOIN clients c ON cs.client_id = c.id WHERE c.id IS NULL OR c.deleted = 1" ); } 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; $value = $mdb -> get( 'settings', 'setting_value', [ 'setting_key' => $setting_key ] ); if ( $value === null || $value === false ) { return $default; } return $value; } static private function set_setting_value( $setting_key, $setting_value ) { global $mdb; if ( $mdb -> count( 'settings', [ 'setting_key' => $setting_key ] ) ) { $mdb -> update( 'settings', [ 'setting_value' => $setting_value ], [ 'setting_key' => $setting_key ] ); return; } $mdb -> insert( 'settings', [ 'setting_key' => $setting_key, 'setting_value' => $setting_value ] ); } static private function touch_cron_invocation( $action_name ) { $now_timestamp = time(); $now = date( 'Y-m-d H:i:s', $now_timestamp ); $last_action_invoked_at = self::get_setting_value( 'cron_last_invoked_' . $action_name . '_at', '' ); $last_action_timestamp = strtotime( (string) $last_action_invoked_at ); if ( $last_action_timestamp ) { $interval_seconds = $now_timestamp - $last_action_timestamp; // Pomijamy skrajne wartosci (np. pierwsze uruchomienie po dluzszej przerwie). if ( $interval_seconds >= 1 && $interval_seconds <= 21600 ) { $avg_key = 'cron_avg_interval_' . $action_name . '_sec'; $samples_key = 'cron_avg_interval_' . $action_name . '_samples'; $avg_interval = (float) self::get_setting_value( $avg_key, 0 ); $samples = (int) self::get_setting_value( $samples_key, 0 ); $weight = min( 99, max( 0, $samples ) ); if ( $weight <= 0 || $avg_interval <= 0 ) { $new_avg = (float) $interval_seconds; $new_samples = 1; } else { $new_avg = ( ( $avg_interval * $weight ) + $interval_seconds ) / ( $weight + 1 ); $new_samples = min( 100, $weight + 1 ); } self::set_setting_value( $avg_key, (string) round( $new_avg, 2 ) ); self::set_setting_value( $samples_key, (string) $new_samples ); self::set_setting_value( 'cron_last_interval_' . $action_name . '_sec', (string) (int) $interval_seconds ); } } self::set_setting_value( 'cron_last_invoked_at', $now ); self::set_setting_value( 'cron_last_invoked_' . $action_name . '_at', $now ); } static private function format_bidding_strategy( $strategy_type, $target_roas = 0 ) { $map = [ 'MAXIMIZE_CONVERSIONS' => 'Maksymalizacja liczby konwersji', 'MAXIMIZE_CONVERSION_VALUE' => 'Maksymalizacja wartosci konwersji', 'TARGET_ROAS' => 'Docelowy ROAS', 'TARGET_CPA' => 'Docelowy CPA', 'MANUAL_CPC' => 'Reczny CPC', 'MANUAL_CPM' => 'Reczny CPM', 'TARGET_IMPRESSION_SHARE' => 'Docelowy udzial w wyswietleniach', ]; $label = $map[ $strategy_type ] ?? $strategy_type ?? 'brak'; if ( $target_roas > 0 ) { $label .= ' | docelowy ROAS: ' . round( $target_roas * 100 ) . '%'; } return $label; } // =========================== // FRAZY - history 30 // =========================== static public function cron_phrase_history_30_save( $phrase_id, $date_from, $date_to ) { global $mdb; $data = $mdb -> query( 'SELECT * FROM phrases_history WHERE phrase_id = ' . $phrase_id . ' AND date_add >= \'' . $date_from . '\' AND date_add <= \'' . $date_to . '\' ORDER BY date_add ASC' ) -> fetchAll( \PDO::FETCH_ASSOC ); // Inicjalizacja tablic do przechowywania danych $phrases_data = []; // Grupowanie danych wedug fraz foreach ( $data as $entry ) { $phrase_id = $entry['phrase_id']; if ( !isset( $phrases_data[$phrase_id] ) ) { $phrases_data[$phrase_id] = [ 'impressions' => 0, 'clicks' => 0, 'cost' => 0.0, 'conversions' => 0, 'conversions_value' => 0.0, 'roas' => 0, 'days_counted' => [] ]; } // Sumowanie danych wedug fraz $phrases_data[$phrase_id]['impressions'] += $entry['impressions']; $phrases_data[$phrase_id]['clicks'] += $entry['clicks']; $phrases_data[$phrase_id]['cost'] += $entry['cost']; $phrases_data[$phrase_id]['conversions'] += $entry['conversions']; $phrases_data[$phrase_id]['conversions_value'] += $entry['conversions_value']; $phrases_data[$phrase_id]['days_counted'][] = $entry['date_add']; } foreach ( $phrases_data as $phrase ) { $day_count = count( $phrase['days_counted'] ); $impressions = $phrase['impressions'] / $day_count; $clicks = $phrase['clicks'] / $day_count; $cost = $phrase['cost'] / $day_count; $conversions = $phrase['conversions'] / $day_count; $conversions_value = $phrase['conversions_value'] / $day_count; $roas = ( $conversions_value > 0 and $cost ) ? round( $conversions_value / $cost, 2 ) * 100 : 0; if ( $day_count > 14 ) { if ( $mdb -> count( 'phrases_history_30', [ 'AND' => [ 'phrase_id' => $phrase_id, 'date_add' => $date_to ] ] ) > 0 ) { $mdb -> update( 'phrases_history_30', [ 'impressions' => $impressions, 'clicks' => $clicks, 'cost' => $cost, 'conversions' => $conversions, 'conversions_value' => $conversions_value, 'roas' => $roas ], [ 'AND' => [ 'phrase_id' => $phrase_id, 'date_add' => $date_to ] ] ); } else { $mdb -> insert( 'phrases_history_30', [ 'phrase_id' => $phrase_id, 'impressions' => $impressions, 'clicks' => $clicks, 'cost' => $cost, 'conversions' => $conversions, 'conversions_value' => $conversions_value, 'roas' => $roas, 'date_add' => $date_to ] ); } } } } }