feat: update font to Roboto across templates and add campaign/ad group filters in product views

- Changed font from Open Sans to Roboto in layout files.
- Added campaign and ad group filters in products main view.
- Enhanced product history to include campaign and ad group IDs.
- Updated migrations to support new campaign and ad group dimensions in product statistics.
- Introduced new migration files for managing campaign types and dropping obsolete columns.
This commit is contained in:
2026-02-18 01:21:22 +01:00
parent 1cff9ba0eb
commit 4635cefcbb
23 changed files with 2444 additions and 410 deletions

View File

@@ -295,7 +295,7 @@ class Api
$processed = 0;
$skipped = 0;
$touched_product_ids = [];
$touched_scopes = [];
foreach ( $data['data'] as $offer )
{
@@ -338,6 +338,23 @@ class Api
continue;
}
$campaign_external_id = (int) self::normalize_number( $offer['CampaignId'] ?? ( $offer['campaign_id'] ?? 0 ) );
$campaign_name = trim( (string) ( $offer['CampaignName'] ?? ( $offer['campaign_name'] ?? '' ) ) );
$ad_group_external_id = (int) self::normalize_number( $offer['AdGroupId'] ?? ( $offer['ad_group_id'] ?? 0 ) );
$ad_group_name = trim( (string) ( $offer['AdGroupName'] ?? ( $offer['ad_group_name'] ?? '' ) ) );
$scope = \controls\Cron::resolve_products_scope_ids(
$client_id,
$campaign_external_id,
$campaign_name,
$ad_group_external_id,
$ad_group_name,
$date
);
$db_campaign_id = (int) ( $scope['campaign_id'] ?? 0 );
$db_ad_group_id = (int) ( $scope['ad_group_id'] ?? 0 );
$impressions = (int) round( self::normalize_number( $offer['Impressions'] ?? 0 ) );
$clicks = (int) round( self::normalize_number( $offer['Clicks'] ?? 0 ) );
$cost = self::normalize_number( $offer['Cost'] ?? 0 );
@@ -352,12 +369,24 @@ class Api
'cost' => $cost,
'conversions' => $conversions,
'conversions_value' => $conversion_value,
'updated' => 1
'updated' => 1,
'campaign_id' => $db_campaign_id,
'ad_group_id' => $db_ad_group_id
];
if ( $mdb -> count( 'products_history', [ 'AND' => [ 'product_id' => $product_id, 'date_add' => $date ] ] ) )
if ( $mdb -> count( 'products_history', [ 'AND' => [
'product_id' => $product_id,
'campaign_id' => $db_campaign_id,
'ad_group_id' => $db_ad_group_id,
'date_add' => $date
] ] ) )
{
$offer_data_old = $mdb -> get( 'products_history', '*', [ 'AND' => [ 'product_id' => $product_id, 'date_add' => $date ] ] );
$offer_data_old = $mdb -> get( 'products_history', '*', [ 'AND' => [
'product_id' => $product_id,
'campaign_id' => $db_campaign_id,
'ad_group_id' => $db_ad_group_id,
'date_add' => $date
] ] );
if (
$offer_data_old['impressions'] == $offer_data['impressions']
@@ -367,13 +396,23 @@ class Api
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;
$scope_key = (int) $product_id . '|' . $db_campaign_id . '|' . $db_ad_group_id;
$touched_scopes[ $scope_key ] = [
'product_id' => (int) $product_id,
'campaign_id' => $db_campaign_id,
'ad_group_id' => $db_ad_group_id
];
$processed++;
continue;
}
$mdb -> update( 'products_history', $offer_data, [
'AND' => [ 'product_id' => $product_id, 'date_add' => $date ]
'AND' => [
'product_id' => $product_id,
'campaign_id' => $db_campaign_id,
'ad_group_id' => $db_ad_group_id,
'date_add' => $date
]
] );
}
else
@@ -383,19 +422,33 @@ class Api
$mdb -> insert( 'products_history', $offer_data );
}
$touched_product_ids[ $product_id ] = true;
$scope_key = (int) $product_id . '|' . $db_campaign_id . '|' . $db_ad_group_id;
$touched_scopes[ $scope_key ] = [
'product_id' => (int) $product_id,
'campaign_id' => $db_campaign_id,
'ad_group_id' => $db_ad_group_id
];
$processed++;
}
$history_30_rows = 0;
foreach ( array_keys( $touched_product_ids ) as $product_id )
foreach ( $touched_scopes as $scope )
{
\controls\Cron::cron_product_history_30_save( (int) $product_id, $date );
$mdb -> update( 'products_history', [ 'updated' => 0 ], [ 'AND' => [ 'product_id' => (int) $product_id, 'date_add' => $date ] ] );
$product_id = (int) ( $scope['product_id'] ?? 0 );
$campaign_id = (int) ( $scope['campaign_id'] ?? 0 );
$ad_group_id = (int) ( $scope['ad_group_id'] ?? 0 );
\controls\Cron::cron_product_history_30_save( $product_id, $date, $campaign_id, $ad_group_id );
$mdb -> update( 'products_history', [ 'updated' => 0 ], [ 'AND' => [
'product_id' => $product_id,
'campaign_id' => $campaign_id,
'ad_group_id' => $ad_group_id,
'date_add' => $date
] ] );
$history_30_rows++;
}
$temp_rows = self::rebuild_products_temp_for_client( $client_id );
$temp_rows = \controls\Cron::rebuild_products_temp_for_client( $client_id );
echo json_encode( [
'status' => 'ok',
@@ -411,67 +464,7 @@ class Api
static private function rebuild_products_temp_for_client( $client_id )
{
global $mdb;
$client_id = (int) $client_id;
if ( $client_id <= 0 )
{
return 0;
}
$product_ids = $mdb -> select( 'products', 'id', [ 'client_id' => $client_id ] );
if ( empty( $product_ids ) )
{
return 0;
}
$mdb -> delete( 'products_temp', [ 'product_id' => $product_ids ] );
$rows = $mdb -> query(
'SELECT p.id AS product_id, p.name,
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
WHERE p.client_id = :client_id
GROUP BY p.id, p.name',
[ ':client_id' => $client_id ]
) -> fetchAll( \PDO::FETCH_ASSOC );
$inserted = 0;
foreach ( $rows as $row )
{
$impressions = (int) $row['impressions'];
$clicks = (int) $row['clicks'];
$cost = (float) $row['cost'];
$conversions = (float) $row['conversions'];
$conversion_value = (float) $row['conversions_value'];
$ctr = ( $impressions > 0 ) ? round( $clicks / $impressions, 4 ) * 100 : 0;
$cpc = ( $clicks > 0 ) ? round( $cost / $clicks, 6 ) : 0;
$roas = ( $cost > 0 ) ? round( $conversion_value / $cost, 2 ) * 100 : 0;
$mdb -> insert( 'products_temp', [
'product_id' => (int) $row['product_id'],
'name' => $row['name'],
'impressions' => $impressions,
'impressions_30' => (int) \factory\Products::get_impressions_30( (int) $row['product_id'] ),
'clicks' => $clicks,
'clicks_30' => (int) \factory\Products::get_clicks_30( (int) $row['product_id'] ),
'ctr' => $ctr,
'cost' => $cost,
'conversions' => $conversions,
'conversions_value' => $conversion_value,
'cpc' => $cpc,
'roas' => $roas
] );
$inserted++;
}
return $inserted;
return \controls\Cron::rebuild_products_temp_for_client( $client_id );
}
static private function normalize_number( $value )

View File

@@ -13,7 +13,7 @@ class CampaignTerms
static public function get_campaigns_list()
{
$client_id = (int) \S::get( 'client_id' );
echo json_encode( [ 'campaigns' => \factory\Campaigns::get_campaigns_list( $client_id ) ] );
echo json_encode( [ 'campaigns' => \factory\Campaigns::get_campaigns_list( $client_id, true ) ] );
exit;
}
@@ -54,6 +54,7 @@ class CampaignTerms
$search_term_id = (int) \S::get( 'search_term_id' );
$match_type = strtoupper( trim( (string) \S::get( 'match_type' ) ) );
$scope = strtolower( trim( (string) \S::get( 'scope' ) ) );
$manual_keyword_text = trim( (string) \S::get( 'keyword_text' ) );
if ( $search_term_id <= 0 )
{
@@ -80,7 +81,9 @@ class CampaignTerms
$customer_id = trim( (string) ( $context['google_ads_customer_id'] ?? '' ) );
$campaign_external_id = trim( (string) ( $context['external_campaign_id'] ?? '' ) );
$ad_group_external_id = trim( (string) ( $context['external_ad_group_id'] ?? '' ) );
$keyword_text = trim( (string) ( $context['search_term'] ?? '' ) );
$context_keyword_text = trim( (string) ( $context['search_term'] ?? '' ) );
$keyword_text = $manual_keyword_text !== '' ? $manual_keyword_text : $context_keyword_text;
$keyword_source = $manual_keyword_text !== '' ? 'manual' : 'search_term';
$missing_data = ( $customer_id === '' || $keyword_text === '' );
if ( $scope === 'campaign' && $campaign_external_id === '' )
@@ -102,6 +105,7 @@ class CampaignTerms
'campaign_external_id' => $campaign_external_id,
'ad_group_external_id' => $ad_group_external_id,
'keyword_text' => $keyword_text,
'keyword_source' => $keyword_source,
'scope' => $scope,
'context' => $context
]
@@ -137,6 +141,7 @@ class CampaignTerms
'campaign_external_id' => $campaign_external_id,
'ad_group_external_id' => $ad_group_external_id,
'keyword_text' => $keyword_text,
'keyword_source' => $keyword_source,
'match_type' => $match_type,
'scope' => $scope,
'api_result' => $api_result
@@ -166,6 +171,7 @@ class CampaignTerms
'campaign_external_id' => $campaign_external_id,
'ad_group_external_id' => $ad_group_external_id,
'keyword_text' => $keyword_text,
'keyword_source' => $keyword_source,
'scope' => $scope,
'api_response' => $api_result['response'] ?? null,
'sent_operation' => $api_result['sent_operation'] ?? null,

View File

@@ -293,6 +293,23 @@ class Cron
continue;
}
$campaign_external_id = (int) ( $offer['CampaignId'] ?? 0 );
$campaign_name = trim( (string) ( $offer['CampaignName'] ?? '' ) );
$ad_group_external_id = (int) ( $offer['AdGroupId'] ?? 0 );
$ad_group_name = trim( (string) ( $offer['AdGroupName'] ?? '' ) );
$scope = self::resolve_products_scope_ids(
$client_id,
$campaign_external_id,
$campaign_name,
$ad_group_external_id,
$ad_group_name,
$date
);
$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 );
@@ -307,12 +324,24 @@ class Cron
'cost' => $cost,
'conversions' => $conversions,
'conversions_value' => $conversion_value,
'updated' => 1
'updated' => 1,
'campaign_id' => $db_campaign_id,
'ad_group_id' => $db_ad_group_id
];
if ( $mdb -> count( 'products_history', [ 'AND' => [ 'product_id' => $product_id, 'date_add' => $date ] ] ) )
if ( $mdb -> count( 'products_history', [ 'AND' => [
'product_id' => $product_id,
'campaign_id' => $db_campaign_id,
'ad_group_id' => $db_ad_group_id,
'date_add' => $date
] ] ) )
{
$offer_data_old = $mdb -> get( 'products_history', '*', [ 'AND' => [ 'product_id' => $product_id, 'date_add' => $date ] ] );
$offer_data_old = $mdb -> get( 'products_history', '*', [ 'AND' => [
'product_id' => $product_id,
'campaign_id' => $db_campaign_id,
'ad_group_id' => $db_ad_group_id,
'date_add' => $date
] ] );
if (
$offer_data_old['impressions'] == $offer_data['impressions']
@@ -328,7 +357,12 @@ class Cron
}
$mdb -> update( 'products_history', $offer_data, [
'AND' => [ 'product_id' => $product_id, 'date_add' => $date ]
'AND' => [
'product_id' => $product_id,
'campaign_id' => $db_campaign_id,
'ad_group_id' => $db_ad_group_id,
'date_add' => $date
]
] );
}
else
@@ -351,6 +385,163 @@ class Cron
];
}
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 )
{
global $mdb;
@@ -359,156 +550,200 @@ class Cron
$date = date( 'Y-m-d', strtotime( $date ) );
$rows = $mdb -> query(
'SELECT ph.id, ph.product_id, ph.date_add
'SELECT DISTINCT ph.product_id, ph.campaign_id, ph.ad_group_id, ph.date_add
FROM products_history AS ph
INNER JOIN products AS p ON p.id = ph.product_id
WHERE p.client_id = ' . $client_id . '
AND ph.updated = 1
AND ph.date_add = \'' . $date . '\'
ORDER BY ph.product_id ASC'
ORDER BY ph.product_id ASC, ph.campaign_id ASC, ph.ad_group_id ASC'
) -> fetchAll( \PDO::FETCH_ASSOC );
$processed = 0;
foreach ( $rows as $row )
{
$product_id = (int) $row['product_id'];
self::cron_product_history_30_save( $product_id, $row['date_add'] );
$mdb -> update( 'products_history', [ 'updated' => 0 ], [ 'id' => (int) $row['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 private function rebuild_products_temp_for_client( $client_id )
static public function rebuild_products_temp_for_client( $client_id )
{
global $mdb;
$client_bestseller_min_roas = \factory\Products::get_client_bestseller_min_roas( $client_id );
$db_result = $mdb -> query( 'SELECT * FROM products AS p INNER JOIN products_history AS ph ON p.id = ph.product_id WHERE p.client_id = ' . (int) $client_id ) -> fetchAll( \PDO::FETCH_ASSOC );
$aggregated_data = [];
foreach ( $db_result as $row )
$client_id = (int) $client_id;
if ( $client_id <= 0 )
{
$product_id = (int) $row['product_id'];
if ( !isset( $aggregated_data[$client_id] ) )
{
$aggregated_data[$client_id] = [];
}
if ( !isset( $aggregated_data[$client_id][$product_id] ) )
{
$aggregated_data[$client_id][$product_id] = [
'product_id' => $product_id,
'name' => $row['name'],
'impressions' => 0,
'clicks' => 0,
'cost' => 0.0,
'conversions' => 0,
'conversions_value' => 0.0
];
}
$aggregated_data[$client_id][$product_id]['impressions'] += (int) $row['impressions'];
$aggregated_data[$client_id][$product_id]['clicks'] += (int) $row['clicks'];
$aggregated_data[$client_id][$product_id]['cost'] += (float) $row['cost'];
$aggregated_data[$client_id][$product_id]['conversions'] += (float) $row['conversions'];
$aggregated_data[$client_id][$product_id]['conversions_value'] += (float) $row['conversions_value'];
return 0;
}
$products_ids = $mdb -> select( 'products', 'id', [ 'client_id' => (int) $client_id ] );
$products_ids_array = [];
foreach ( $products_ids as $product_id )
$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
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 ) )
{
$products_ids_array[] = (int) $product_id;
$mdb -> delete( 'products_temp', [ 'product_id' => $product_ids ] );
}
if ( !empty( $products_ids_array ) )
// 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
WHERE p.client_id = :client_id
GROUP BY p.id',
[ ':client_id' => $client_id ]
) -> fetchAll( \PDO::FETCH_ASSOC );
foreach ( $global_totals as $total )
{
$mdb -> delete( 'products_temp', [ 'product_id' => $products_ids_array ] );
$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'] ) )
{
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'] ] );
}
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' )
] );
}
}
}
}
$processed_rows = 0;
foreach ( $aggregated_data as $client_offers )
foreach ( $rows as $row )
{
foreach ( $client_offers as $offer_data )
$product_id = (int) ( $row['product_id'] ?? 0 );
if ( $product_id <= 0 )
{
// Obliczamy wartoci CPC oraz ROAS
$cpc = $offer_data['clicks'] > 0 ? round( $offer_data['cost'] / $offer_data['clicks'], 6 ) : 0;
$roas = ( $offer_data['conversions'] > 0 and $offer_data['cost'] ) ? round( $offer_data['conversions_value'] / $offer_data['cost'], 2 ) * 100 : 0;
$impressions_30 = \factory\Products::get_impressions_30( $offer_data['product_id'] );
// update custom_label_4 only current is empty or is bestseller
$custom_label_4 = \factory\Products::get_product_data( $offer_data['product_id'], 'custom_label_4' );
if ( $custom_label_4 == null || ( $custom_label_4 == 'bestseller' and (int)$client_bestseller_min_roas > 0 ) )
{
if ( $roas > $client_bestseller_min_roas and $offer_data['conversions'] > 10 )
{
$new_custom_label_4 = 'bestseller';
}
else
{
$new_custom_label_4 = null;
}
$offers_data_tmp = $mdb -> get( 'products_data', '*', [ 'product_id' => $offer_data['product_id'] ] );
if ( isset( $offers_data_tmp['id'] ) )
{
if ( $new_custom_label_4 != $offers_data_tmp['custom_label_4'] )
$mdb -> insert( 'products_comments', [
'product_id' => $offer_data['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'] ] );
}
else
{
$mdb -> insert( 'products_data', [
'product_id' => $offer_data['product_id'],
'custom_label_4' => $new_custom_label_4
] );
if ( $new_custom_label_4 == 'bestseller' )
{
$mdb -> insert( 'products_comments', [
'product_id' => $offer_data['product_id'],
'comment' => 'Zmiana pola "custom_label_4" na: bestseller',
'type' => 1,
'date_add' => date( 'Y-m-d' )
] );
}
}
}
$clicks_30 = \factory\Products::get_clicks_30( $offer_data['product_id'] );
$mdb -> insert( 'products_temp', [
'product_id' => $offer_data['product_id'],
'name' => $offer_data['name'],
'impressions' => $offer_data['impressions'],
'impressions_30' => $impressions_30,
'clicks' => $offer_data['clicks'],
'clicks_30' => $clicks_30,
'ctr' => ( $offer_data['impressions'] > 0 ) ? round( $offer_data['clicks'] / $offer_data['impressions'], 4 ) * 100 : 0,
'cost' => $offer_data['cost'],
'conversions' => $offer_data['conversions'],
'conversions_value' => $offer_data['conversions_value'],
'cpc' => $cpc,
'roas' => $roas,
] );
$processed_rows++;
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;
@@ -537,11 +772,20 @@ class Cron
$products = $mdb -> select( 'products', 'id', [ 'client_id' => $client_id ] );
foreach ( $products as $product )
{
$dates = $mdb -> query( 'SELECT id, date_add FROM products_history WHERE product_id = ' . $product . ' AND updated = 1 ORDER BY date_add DESC' ) -> fetchAll( \PDO::FETCH_ASSOC );
foreach ( $dates as $date )
$scopes = $mdb -> query( 'SELECT DISTINCT campaign_id, ad_group_id, date_add FROM products_history WHERE product_id = ' . $product . ' AND updated = 1 ORDER BY date_add DESC' ) -> fetchAll( \PDO::FETCH_ASSOC );
foreach ( $scopes as $scope )
{
self::cron_product_history_30_save( $product, $date['date_add'] );
$mdb -> update( 'products_history', [ 'updated' => 0 ], [ 'id' => $date['id'] ] );
$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
] ] );
}
}
@@ -551,19 +795,61 @@ class Cron
exit;
}
static public function get_roas_all_time( $product_id, $date_to )
static public function get_roas_all_time( $product_id, $date_to, $campaign_id = 0, $ad_group_id = 0 )
{
global $mdb;
$roas_all_time = $mdb -> query( 'SELECT SUM(conversions_value) / SUM(cost) * 100 AS roas_all_time FROM products_history WHERE product_id = ' . $product_id . ' AND date_add <= \'' . $date_to . '\'' ) -> fetchColumn();
$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 )
static public function cron_product_history_30_save( $product_id, $date_to, $campaign_id = 0, $ad_group_id = 0 )
{
global $mdb;
$data = $mdb -> query( 'SELECT * FROM products_history WHERE product_id = ' . $product_id . ' AND date_add <= \'' . $date_to . '\' ORDER BY date_add DESC LIMIT 30' ) -> fetchAll( \PDO::FETCH_ASSOC );
$product_id = (int) $product_id;
$campaign_id = (int) $campaign_id;
$ad_group_id = (int) $ad_group_id;
$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 = [];
@@ -605,9 +891,29 @@ class Cron
$conversions_value = $offer['conversions_value'];
$roas = ( $conversions_value > 0 and $cost ) ? round( $conversions_value / $cost, 2 ) * 100 : 0;
if ( $mdb -> count( 'products_history', [ 'AND' => [ 'product_id' => $product_id, 'date_add[<=]' => $date_to ] ] ) >= 14 )
$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, 'date_add' => $date_to ] ] ) > 0 )
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,
@@ -617,13 +923,20 @@ class Cron
'conversions' => $conversions,
'conversions_value' => $conversions_value,
'roas' => $roas,
'roas_all_time' => self::get_roas_all_time( $product_id, $date_to )
], [ 'AND' => [ 'product_id' => $product_id, 'date_add' => $date_to ] ] );
'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,
@@ -631,7 +944,7 @@ class Cron
'conversions' => $conversions,
'conversions_value' => $conversions_value,
'roas' => $roas,
'roas_all_time' => self::get_roas_all_time( $product_id, $date_to ),
'roas_all_time' => self::get_roas_all_time( $product_id, $date_to, $campaign_id, $ad_group_id ),
'date_add' => $date_to
] );
}
@@ -1032,6 +1345,8 @@ class Cron
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 );
@@ -1044,7 +1359,8 @@ class Cron
$mdb -> insert( 'campaigns', [
'client_id' => $client['id'],
'campaign_id' => $external_campaign_id,
'campaign_name' => $campaign['campaign_name']
'campaign_name' => $campaign['campaign_name'],
'advertising_channel_type' => $advertising_channel_type !== '' ? $advertising_channel_type : null
] );
$db_campaign_id = $mdb -> id();
}
@@ -1056,7 +1372,8 @@ class Cron
] ] );
$mdb -> update( 'campaigns', [
'campaign_name' => $campaign['campaign_name']
'campaign_name' => $campaign['campaign_name'],
'advertising_channel_type' => $advertising_channel_type !== '' ? $advertising_channel_type : null
], [ 'id' => $db_campaign_id ] );
}
@@ -1111,7 +1428,8 @@ class Cron
$mdb -> insert( 'campaigns', [
'client_id' => $client['id'],
'campaign_id' => 0,
'campaign_name' => '--- konto ---'
'campaign_name' => '--- konto ---',
'advertising_channel_type' => null
] );
$db_account_campaign_id = $mdb -> id();
}
@@ -1123,7 +1441,8 @@ class Cron
] ] );
$mdb -> update( 'campaigns', [
'campaign_name' => '--- konto ---'
'campaign_name' => '--- konto ---',
'advertising_channel_type' => null
], [ 'id' => $db_account_campaign_id ] );
}
@@ -1358,6 +1677,15 @@ class Cron
$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;
@@ -1404,6 +1732,50 @@ class Cron
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_negative_keywords_for_client( $campaigns_db_map, $ad_group_db_map, $customer_id, $api, $date_sync )
{
global $mdb;

View File

@@ -36,6 +36,27 @@ class Products
] );
}
static public function get_campaigns_list()
{
$client_id = (int) \S::get( 'client_id' );
echo json_encode( [ 'campaigns' => \factory\Campaigns::get_campaigns_list( $client_id, true ) ] );
exit;
}
static public function get_campaign_ad_groups()
{
$campaign_id = (int) \S::get( 'campaign_id' );
if ( $campaign_id <= 0 )
{
echo json_encode( [ 'ad_groups' => [] ] );
exit;
}
echo json_encode( [ 'ad_groups' => \factory\Campaigns::get_campaign_ad_groups( $campaign_id ) ] );
exit;
}
static public function comment_add()
{
$product_id = \S::get( 'product_id' );
@@ -178,6 +199,8 @@ class Products
static public function get_products()
{
$client_id = \S::get( 'client_id' );
$campaign_id = (int) \S::get( 'campaign_id' );
$ad_group_id = (int) \S::get( 'ad_group_id' );
$limit = \S::get( 'length' ) ? \S::get( 'length' ) : 10;
$start = \S::get( 'start' ) ? \S::get( 'start' ) : 0;
@@ -186,7 +209,7 @@ class Products
$search = $_POST['search']['value'];
// ➊ MIN/MAX ROAS dla kontekstu klienta (opcjonalnie z filtrem search)
$bounds = \factory\Products::get_roas_bounds( $client_id, $search );
$bounds = \factory\Products::get_roas_bounds( (int) $client_id, $search, $campaign_id, $ad_group_id );
$roas_min = (float)$bounds['min'];
$roas_max = (float)$bounds['max'];
// zabezpieczenie przed dzieleniem przez 0
@@ -221,12 +244,13 @@ class Products
</div>';
};
$db_results = \factory\Products::get_products( $client_id, $search, $limit, $start, $order_name, $order_dir );
$recordsTotal = \factory\Products::get_records_total_products( $client_id, $search );
$db_results = \factory\Products::get_products( $client_id, $search, $limit, $start, $order_name, $order_dir, $campaign_id, $ad_group_id );
$recordsTotal = \factory\Products::get_records_total_products( $client_id, $search, $campaign_id, $ad_group_id );
$data['draw'] = \S::get( 'draw' );
$data['recordsTotal'] = $recordsTotal;
$data['recordsFiltered'] = $recordsTotal;
$data['data'] = [];
foreach ( $db_results as $row )
{
@@ -270,8 +294,10 @@ class Products
'', // checkbox column
$row['product_id'],
$row['offer_id'],
htmlspecialchars( (string) ( $row['campaign_name'] ?? '' ) ),
htmlspecialchars( (string) ( $row['ad_group_name'] ?? '' ) ),
'<div class="table-product-title" product_id="' . $row['product_id'] . '">
<a href="/products/product_history/client_id=' . $client_id . '&product_id=' . $row['product_id'] . '" target="_blank" class="' . $custom_class . '">
<a href="/products/product_history/client_id=' . $client_id . '&product_id=' . $row['product_id'] . '&campaign_id=' . (int) ( $row['campaign_id'] ?? 0 ) . '&ad_group_id=' . (int) ( $row['ad_group_id'] ?? 0 ) . '" target="_blank" class="' . $custom_class . '">
' . $row['name'] . '
</a>
<span class="edit-product-title" product_id="' . $row['product_id'] . '">
@@ -357,10 +383,14 @@ class Products
{
$client_id = \S::get( 'client_id' );
$product_id = \S::get( 'product_id' );
$campaign_id = (int) \S::get( 'campaign_id' );
$ad_group_id = (int) \S::get( 'ad_group_id' );
return \Tpl::view( 'products/product_history', [
'client_id' => $client_id,
'product_id' => $product_id,
'campaign_id' => $campaign_id,
'ad_group_id' => $ad_group_id,
'min_roas' => \factory\Products::get_min_roas( $product_id )
] );
}
@@ -369,15 +399,18 @@ class Products
{
$client_id= \S::get( 'client_id' );
$product_id = \S::get( 'product_id' );
$campaign_id = (int) \S::get( 'campaign_id' );
$ad_group_id = (int) \S::get( 'ad_group_id' );
$start = \S::get( 'start' ) ? \S::get( 'start' ) : 0;
$limit = \S::get( 'length' ) ? \S::get( 'length' ) : 10;
$db_results = \factory\Products::get_product_history( $client_id, $product_id, $start, $limit );
$recordsTotal = \factory\Products::get_records_total_product_history( $client_id, $product_id );
$db_results = \factory\Products::get_product_history( $client_id, $product_id, $start, $limit, $campaign_id, $ad_group_id );
$recordsTotal = \factory\Products::get_records_total_product_history( $client_id, $product_id, $campaign_id, $ad_group_id );
$data['draw'] = \S::get( 'draw' );
$data['recordsTotal'] = $recordsTotal;
$data['recordsFiltered'] = $recordsTotal;
$data['data'] = [];
foreach ( $db_results as $row )
{
@@ -416,13 +449,16 @@ class Products
{
$client_id = \S::get( 'client_id' );
$product_id = \S::get( 'product_id' );
$campaign_id = (int) \S::get( 'campaign_id' );
$ad_group_id = (int) \S::get( 'ad_group_id' );
$limit = \S::get( 'length' ) ? \S::get( 'length' ) : 360;
$start = \S::get( 'start' ) ? \S::get( 'start' ) : 0;
$db_results = \factory\Products::get_product_history_30( $client_id, $product_id, $start, $limit );
$db_results = \factory\Products::get_product_history_30( $client_id, $product_id, $start, $limit, $campaign_id, $ad_group_id );
$impressions = [];
$clicks = [];
$ctr = [];
$cost = [];
$conversions = [];
$conversions_value = [];
@@ -507,4 +543,4 @@ class Products
echo json_encode( [ 'status' => 'ok' ] );
exit;
}
}
}

View File

@@ -8,10 +8,45 @@ class Campaigns
return $mdb -> select( 'clients', '*', [ 'ORDER' => [ 'name' => 'ASC' ] ] );
}
static public function get_campaigns_list( $client_id )
static public function get_campaigns_list( $client_id, $only_active = false )
{
global $mdb;
return $mdb -> select( 'campaigns', '*', [ 'client_id' => $client_id, 'ORDER' => [ 'campaign_name' => 'ASC' ] ] );
$client_id = (int) $client_id;
if ( !$only_active )
{
return $mdb -> select( 'campaigns', '*', [ 'client_id' => $client_id, 'ORDER' => [ 'campaign_name' => 'ASC' ] ] );
}
$latest_date = $mdb -> query(
'SELECT MAX( ch.date_add )
FROM campaigns_history AS ch
INNER JOIN campaigns AS c ON c.id = ch.campaign_id
WHERE c.client_id = :client_id
AND c.campaign_id <> 0',
[ ':client_id' => $client_id ]
) -> fetchColumn();
if ( !$latest_date )
{
return $mdb -> select( 'campaigns', '*', [ 'client_id' => $client_id, 'ORDER' => [ 'campaign_name' => 'ASC' ] ] );
}
return $mdb -> query(
'SELECT c.*
FROM campaigns AS c
LEFT JOIN campaigns_history AS ch
ON ch.campaign_id = c.id
AND ch.date_add = :latest_date
WHERE c.client_id = :client_id
AND ( c.campaign_id = 0 OR ch.id IS NOT NULL )
ORDER BY c.campaign_name ASC',
[
':client_id' => $client_id,
':latest_date' => $latest_date
]
) -> fetchAll( \PDO::FETCH_ASSOC );
}
static public function get_campaign_history_data( $campaign_id, $start, $length, $revert = false )
@@ -144,6 +179,7 @@ class Campaigns
st.ad_group_id AS db_ad_group_id,
c.client_id,
c.campaign_id AS external_campaign_id,
c.advertising_channel_type,
ag.ad_group_id AS external_ad_group_id,
cl.google_ads_customer_id
FROM campaign_search_terms AS st

View File

@@ -61,63 +61,181 @@ class Products
return $mdb -> update( 'products', [ 'min_roas' => $min_roas ], [ 'id' => $product_id ] );
}
static public function get_products( $client_id, $search, $limit, $start, $order_name, $order_dir )
static private function build_scope_filters( &$sql, &$params, $campaign_id, $ad_group_id )
{
global $mdb;
$campaign_id = (int) $campaign_id;
$ad_group_id = (int) $ad_group_id;
if ( $search )
return $mdb -> query( 'SELECT pt.*, p.offer_id, p.min_roas FROM products_temp AS pt INNER JOIN products AS p ON p.id = pt.product_id WHERE client_id = \'' . $client_id . '\' AND ( pt.name LIKE \'%' . $search . '%\' OR offer_id LIKE \'%' . $search . '%\' ) ORDER BY ' . $order_name . ' ' . $order_dir . ', id DESC LIMIT ' . $start . ', ' . $limit ) -> fetchAll();
else
return $mdb -> query( 'SELECT pt.*, p.offer_id, p.min_roas FROM products_temp AS pt INNER JOIN products AS p ON p.id = pt.product_id WHERE client_id = \'' . $client_id . '\' ORDER BY ' . $order_name . ' ' . $order_dir . ', id DESC LIMIT ' . $start . ', ' . $limit ) -> fetchAll();
if ( $campaign_id > 0 )
{
$sql .= ' AND pt.campaign_id = :campaign_id';
$params[':campaign_id'] = $campaign_id;
}
if ( $ad_group_id > 0 )
{
$sql .= ' AND pt.ad_group_id = :ad_group_id';
$params[':ad_group_id'] = $ad_group_id;
}
}
// \factory\Products.php
public static function get_roas_bounds(int $client_id, ?string $search = null): array
static public function get_products( $client_id, $search, $limit, $start, $order_name, $order_dir, $campaign_id = 0, $ad_group_id = 0 )
{
global $mdb;
$params = [':client_id' => $client_id];
$limit = max( 1, (int) $limit );
$start = max( 0, (int) $start );
$order_dir = strtoupper( (string) $order_dir ) === 'ASC' ? 'ASC' : 'DESC';
$sql = 'SELECT MIN(p.min_roas) AS min_roas, MAX(pt.roas) AS max_roas
$order_map = [
'offer_id' => 'p.offer_id',
'campaign_name' => 'c.campaign_name',
'ad_group_name' => 'ag.ad_group_name',
'name' => 'pt.name',
'impressions' => 'pt.impressions',
'impressions_30' => 'pt.impressions_30',
'clicks' => 'pt.clicks',
'clicks_30' => 'pt.clicks_30',
'ctr' => 'pt.ctr',
'cost' => 'pt.cost',
'cpc' => 'pt.cpc',
'conversions' => 'pt.conversions',
'conversions_value' => 'pt.conversions_value',
'roas' => 'pt.roas',
'min_roas' => 'p.min_roas'
];
$order_sql = $order_map[ $order_name ] ?? 'pt.clicks';
$params = [ ':client_id' => (int) $client_id ];
$sql = 'SELECT pt.*, p.offer_id, p.min_roas,
COALESCE( c.campaign_name, \'--- brak kampanii ---\' ) AS campaign_name,
CASE
WHEN pt.ad_group_id = 0 THEN \'PMax (bez grup reklam)\'
ELSE COALESCE( ag.ad_group_name, \'--- brak grupy reklam ---\' )
END AS ad_group_name
FROM products_temp AS pt
INNER JOIN products AS p ON p.id = pt.product_id
WHERE p.client_id = :client_id AND conversions > 10';
LEFT JOIN campaigns AS c ON c.id = pt.campaign_id
LEFT JOIN campaign_ad_groups AS ag ON ag.id = pt.ad_group_id
WHERE p.client_id = :client_id';
if ($search) {
$sql .= ' AND (pt.name LIKE :search OR p.offer_id LIKE :search)';
self::build_scope_filters( $sql, $params, $campaign_id, $ad_group_id );
if ( $search )
{
$sql .= ' AND (
pt.name LIKE :search
OR p.offer_id LIKE :search
OR c.campaign_name LIKE :search
OR ag.ad_group_name LIKE :search
)';
$params[':search'] = '%' . $search . '%';
}
$row = $mdb->query($sql, $params)->fetch(\PDO::FETCH_ASSOC);
$sql .= ' ORDER BY ' . $order_sql . ' ' . $order_dir . ', pt.id DESC LIMIT ' . $start . ', ' . $limit;
return [
'min' => isset($row['min_roas']) ? (float)$row['min_roas'] : 0.0,
'max' => isset($row['max_roas']) ? (float)$row['max_roas'] : 0.0,
];
return $mdb -> query( $sql, $params ) -> fetchAll( \PDO::FETCH_ASSOC );
}
static public function get_records_total_products( $client_id, $search )
public static function get_roas_bounds( int $client_id, ?string $search = null, int $campaign_id = 0, int $ad_group_id = 0 ): array
{
global $mdb;
$params = [ ':client_id' => $client_id ];
$sql = 'SELECT MIN( p.min_roas ) AS min_roas, MAX( pt.roas ) AS max_roas
FROM products_temp AS pt
INNER JOIN products AS p ON p.id = pt.product_id
LEFT JOIN campaigns AS c ON c.id = pt.campaign_id
LEFT JOIN campaign_ad_groups AS ag ON ag.id = pt.ad_group_id
WHERE p.client_id = :client_id
AND pt.conversions > 10';
self::build_scope_filters( $sql, $params, $campaign_id, $ad_group_id );
if ( $search )
return $mdb -> query( 'SELECT COUNT(0) FROM products_temp AS pt INNER JOIN products AS p ON p.id = pt.product_id WHERE client_id = \'' . $client_id . '\' AND ( pt.name LIKE \'%' . $search . '%\' OR offer_id LIKE \'%' . $search . '%\' )' ) -> fetchColumn();
else
return $mdb -> query( 'SELECT COUNT(0) FROM products_temp AS pt INNER JOIN products AS p ON p.id = pt.product_id WHERE client_id = \'' . $client_id . '\'' ) -> fetchColumn();
{
$sql .= ' AND (
pt.name LIKE :search
OR p.offer_id LIKE :search
OR c.campaign_name LIKE :search
OR ag.ad_group_name LIKE :search
)';
$params[':search'] = '%' . $search . '%';
}
$row = $mdb -> query( $sql, $params ) -> fetch( \PDO::FETCH_ASSOC );
return [
'min' => isset( $row['min_roas'] ) ? (float) $row['min_roas'] : 0.0,
'max' => isset( $row['max_roas'] ) ? (float) $row['max_roas'] : 0.0,
];
}
static public function get_records_total_products( $client_id, $search, $campaign_id = 0, $ad_group_id = 0 )
{
global $mdb;
$params = [ ':client_id' => (int) $client_id ];
$sql = 'SELECT COUNT(0)
FROM products_temp AS pt
INNER JOIN products AS p ON p.id = pt.product_id
LEFT JOIN campaigns AS c ON c.id = pt.campaign_id
LEFT JOIN campaign_ad_groups AS ag ON ag.id = pt.ad_group_id
WHERE p.client_id = :client_id';
self::build_scope_filters( $sql, $params, $campaign_id, $ad_group_id );
if ( $search )
{
$sql .= ' AND (
pt.name LIKE :search
OR p.offer_id LIKE :search
OR c.campaign_name LIKE :search
OR ag.ad_group_name LIKE :search
)';
$params[':search'] = '%' . $search . '%';
}
return $mdb -> query( $sql, $params ) -> fetchColumn();
}
static public function get_product_full_context( $product_id )
{
global $mdb;
return $mdb -> query(
'SELECT p.id, p.offer_id, p.name, p.min_roas,
pt.impressions, pt.impressions_30, pt.clicks, pt.clicks_30,
pt.ctr, pt.cost, pt.cpc, pt.conversions, pt.conversions_value, pt.roas
'SELECT
p.id,
p.offer_id,
p.name,
p.min_roas,
COALESCE( SUM( pt.impressions ), 0 ) AS impressions,
COALESCE( SUM( pt.impressions_30 ), 0 ) AS impressions_30,
COALESCE( SUM( pt.clicks ), 0 ) AS clicks,
COALESCE( SUM( pt.clicks_30 ), 0 ) AS clicks_30,
CASE
WHEN COALESCE( SUM( pt.impressions ), 0 ) > 0
THEN ROUND( COALESCE( SUM( pt.clicks ), 0 ) / COALESCE( SUM( pt.impressions ), 0 ) * 100, 2 )
ELSE 0
END AS ctr,
COALESCE( SUM( pt.cost ), 0 ) AS cost,
CASE
WHEN COALESCE( SUM( pt.clicks ), 0 ) > 0
THEN ROUND( COALESCE( SUM( pt.cost ), 0 ) / COALESCE( SUM( pt.clicks ), 0 ), 6 )
ELSE 0
END AS cpc,
COALESCE( SUM( pt.conversions ), 0 ) AS conversions,
COALESCE( SUM( pt.conversions_value ), 0 ) AS conversions_value,
CASE
WHEN COALESCE( SUM( pt.cost ), 0 ) > 0
THEN ROUND( COALESCE( SUM( pt.conversions_value ), 0 ) / COALESCE( SUM( pt.cost ), 0 ) * 100, 2 )
ELSE 0
END AS roas
FROM products AS p
LEFT JOIN products_temp AS pt ON pt.product_id = p.id
WHERE p.id = :pid',
WHERE p.id = :pid
GROUP BY p.id, p.offer_id, p.name, p.min_roas',
[ ':pid' => $product_id ]
) -> fetch( \PDO::FETCH_ASSOC );
}
@@ -139,34 +257,139 @@ class Products
return $result;
}
static public function get_product_history( $client_id, $product_id, $start, $limit )
static public function get_product_history( $client_id, $product_id, $start, $limit, $campaign_id = 0, $ad_group_id = 0 )
{
global $mdb;
return $mdb -> query( 'SELECT * FROM products_history AS ph WHERE ph.product_id = \'' . $product_id . '\' ORDER BY ph.date_add DESC LIMIT ' . $start . ', ' . $limit ) -> fetchAll( \PDO::FETCH_ASSOC );
$limit = max( 1, (int) $limit );
$start = max( 0, (int) $start );
return $mdb -> query(
'SELECT
MAX( ph.id ) AS id,
SUM( ph.impressions ) AS impressions,
SUM( ph.clicks ) AS clicks,
CASE WHEN SUM( ph.impressions ) > 0 THEN ROUND( SUM( ph.clicks ) / SUM( ph.impressions ) * 100, 2 ) ELSE 0 END AS ctr,
SUM( ph.cost ) AS cost,
SUM( ph.conversions ) AS conversions,
SUM( ph.conversions_value ) AS conversions_value,
ph.date_add
FROM products_history AS ph
INNER JOIN products AS p ON p.id = ph.product_id
WHERE ph.product_id = :product_id
AND p.client_id = :client_id
AND ph.campaign_id = :campaign_id
AND ph.ad_group_id = :ad_group_id
GROUP BY ph.date_add
ORDER BY ph.date_add DESC
LIMIT ' . $start . ', ' . $limit,
[
':product_id' => (int) $product_id,
':client_id' => (int) $client_id,
':campaign_id' => (int) $campaign_id,
':ad_group_id' => (int) $ad_group_id
]
) -> fetchAll( \PDO::FETCH_ASSOC );
}
static public function get_records_total_product_history( $client_id, $product_id )
static public function get_records_total_product_history( $client_id, $product_id, $campaign_id = 0, $ad_group_id = 0 )
{
global $mdb;
return $mdb -> query( 'SELECT COUNT(0) FROM products_history AS ph WHERE ph.product_id = \'' . $product_id . '\'' ) -> fetchColumn();
return $mdb -> query(
'SELECT COUNT( DISTINCT ph.date_add )
FROM products_history AS ph
INNER JOIN products AS p ON p.id = ph.product_id
WHERE ph.product_id = :product_id
AND p.client_id = :client_id
AND ph.campaign_id = :campaign_id
AND ph.ad_group_id = :ad_group_id',
[
':product_id' => (int) $product_id,
':client_id' => (int) $client_id,
':campaign_id' => (int) $campaign_id,
':ad_group_id' => (int) $ad_group_id
]
) -> fetchColumn();
}
static public function get_product_history_30( $client_id, $product_id, $start, $limit )
static public function get_product_history_30( $client_id, $product_id, $start, $limit, $campaign_id = 0, $ad_group_id = 0 )
{
global $mdb;
return $mdb -> query( 'SELECT * FROM products_history_30 AS ph3 WHERE ph3.product_id = \'' . $product_id . '\' ORDER BY ph3.date_add ASC LIMIT ' . $start . ', ' . $limit ) -> fetchAll( \PDO::FETCH_ASSOC );
return $mdb -> query(
'SELECT ph3.*
FROM products_history_30 AS ph3
INNER JOIN products AS p ON p.id = ph3.product_id
WHERE ph3.product_id = :product_id
AND p.client_id = :client_id
AND ph3.campaign_id = :campaign_id
AND ph3.ad_group_id = :ad_group_id
ORDER BY ph3.date_add ASC
LIMIT ' . (int) $start . ', ' . (int) $limit,
[
':product_id' => (int) $product_id,
':client_id' => (int) $client_id,
':campaign_id' => (int) $campaign_id,
':ad_group_id' => (int) $ad_group_id
]
) -> fetchAll( \PDO::FETCH_ASSOC );
}
static public function get_impressions_30( $product_id )
static public function get_impressions_30( $product_id, $campaign_id = null, $ad_group_id = null )
{
global $mdb;
return $mdb -> query( 'SELECT SUM(impressions) FROM products_history WHERE product_id = \'' . $product_id . '\' AND date_add >= \'' . date( 'Y-m-d', strtotime( '-30 days', time() ) ) . '\'' ) -> fetchColumn();
$sql = 'SELECT COALESCE( SUM( impressions ), 0 ) AS total
FROM products_history
WHERE product_id = :product_id
AND date_add >= :date_from';
$params = [
':product_id' => (int) $product_id,
':date_from' => date( 'Y-m-d', strtotime( '-30 days', time() ) )
];
if ( $campaign_id !== null )
{
$sql .= ' AND campaign_id = :campaign_id';
$params[':campaign_id'] = (int) $campaign_id;
}
if ( $ad_group_id !== null )
{
$sql .= ' AND ad_group_id = :ad_group_id';
$params[':ad_group_id'] = (int) $ad_group_id;
}
return $mdb -> query( $sql, $params ) -> fetchColumn();
}
static public function get_clicks_30( $product_id )
static public function get_clicks_30( $product_id, $campaign_id = null, $ad_group_id = null )
{
global $mdb;
return $mdb -> query( 'SELECT SUM(clicks) FROM products_history WHERE product_id = \'' . $product_id . '\' AND date_add >= \'' . date( 'Y-m-d', strtotime( '-30 days', time() ) ) . '\'' ) -> fetchColumn();
$sql = 'SELECT COALESCE( SUM( clicks ), 0 ) AS total
FROM products_history
WHERE product_id = :product_id
AND date_add >= :date_from';
$params = [
':product_id' => (int) $product_id,
':date_from' => date( 'Y-m-d', strtotime( '-30 days', time() ) )
];
if ( $campaign_id !== null )
{
$sql .= ' AND campaign_id = :campaign_id';
$params[':campaign_id'] = (int) $campaign_id;
}
if ( $ad_group_id !== null )
{
$sql .= ' AND ad_group_id = :ad_group_id';
$params[':ad_group_id'] = (int) $ad_group_id;
}
return $mdb -> query( $sql, $params ) -> fetchColumn();
}
static public function add_product_comment( $product_id, $comment, $date = null )
@@ -183,4 +406,4 @@ class Products
else
return $mdb -> insert( 'products_comments', [ 'product_id' => $product_id, 'comment' => $comment, 'date_add' => $date ] );
}
}
}

View File

@@ -449,20 +449,49 @@ class GoogleAdsApi
{
$date = date( 'Y-m-d', strtotime( $date ) );
$gaql = "SELECT "
. "segments.date, "
. "segments.product_item_id, "
. "segments.product_title, "
. "metrics.impressions, "
. "metrics.clicks, "
. "metrics.cost_micros, "
. "metrics.conversions, "
. "metrics.conversions_value "
. "FROM shopping_performance_view "
. "WHERE segments.date = '" . $date . "'";
$gaql_with_ad_group = "SELECT "
. "segments.date, "
. "segments.product_item_id, "
. "segments.product_title, "
. "campaign.id, "
. "campaign.name, "
. "ad_group.id, "
. "ad_group.name, "
. "metrics.impressions, "
. "metrics.clicks, "
. "metrics.cost_micros, "
. "metrics.conversions, "
. "metrics.conversions_value "
. "FROM shopping_performance_view "
. "WHERE segments.date = '" . $date . "'";
$results = $this -> search_stream( $customer_id, $gaql );
if ( $results === false ) return false;
$results = $this -> search_stream( $customer_id, $gaql_with_ad_group );
$fallback_without_ad_group = false;
if ( $results === false )
{
$gaql_without_ad_group = "SELECT "
. "segments.date, "
. "segments.product_item_id, "
. "segments.product_title, "
. "campaign.id, "
. "campaign.name, "
. "metrics.impressions, "
. "metrics.clicks, "
. "metrics.cost_micros, "
. "metrics.conversions, "
. "metrics.conversions_value "
. "FROM shopping_performance_view "
. "WHERE segments.date = '" . $date . "'";
$results = $this -> search_stream( $customer_id, $gaql_without_ad_group );
if ( $results === false )
{
return false;
}
$fallback_without_ad_group = true;
}
$products = [];
@@ -474,11 +503,42 @@ class GoogleAdsApi
continue;
}
if ( !isset( $products[ $offer_id ] ) )
$campaign_id = (int) ( $row['campaign']['id'] ?? 0 );
$campaign_name = trim( (string) ( $row['campaign']['name'] ?? '' ) );
if ( $campaign_name === '' && $campaign_id > 0 )
{
$products[ $offer_id ] = [
$campaign_name = 'Kampania #' . $campaign_id;
}
$ad_group_id = 0;
$ad_group_name = 'PMax (bez grup reklam)';
if ( !$fallback_without_ad_group )
{
$ad_group_id = (int) ( $row['adGroup']['id'] ?? 0 );
$ad_group_name = trim( (string) ( $row['adGroup']['name'] ?? '' ) );
if ( $ad_group_id > 0 && $ad_group_name === '' )
{
$ad_group_name = 'Ad group #' . $ad_group_id;
}
else if ( $ad_group_id <= 0 )
{
$ad_group_name = 'PMax (bez grup reklam)';
}
}
$scope_key = $offer_id . '|' . $campaign_id . '|' . $ad_group_id;
if ( !isset( $products[ $scope_key ] ) )
{
$products[ $scope_key ] = [
'OfferId' => $offer_id,
'ProductTitle' => (string) ( $row['segments']['productTitle'] ?? $offer_id ),
'CampaignId' => $campaign_id,
'CampaignName' => $campaign_name,
'AdGroupId' => $ad_group_id,
'AdGroupName' => $ad_group_name,
'Impressions' => 0,
'Clicks' => 0,
'Cost' => 0.0,
@@ -487,11 +547,11 @@ class GoogleAdsApi
];
}
$products[ $offer_id ]['Impressions'] += (int) ( $row['metrics']['impressions'] ?? 0 );
$products[ $offer_id ]['Clicks'] += (int) ( $row['metrics']['clicks'] ?? 0 );
$products[ $offer_id ]['Cost'] += (float) ( $row['metrics']['costMicros'] ?? 0 ) / 1000000;
$products[ $offer_id ]['Conversions'] += (float) ( $row['metrics']['conversions'] ?? 0 );
$products[ $offer_id ]['ConversionValue'] += (float) ( $row['metrics']['conversionsValue'] ?? 0 );
$products[ $scope_key ]['Impressions'] += (int) ( $row['metrics']['impressions'] ?? 0 );
$products[ $scope_key ]['Clicks'] += (int) ( $row['metrics']['clicks'] ?? 0 );
$products[ $scope_key ]['Cost'] += (float) ( $row['metrics']['costMicros'] ?? 0 ) / 1000000;
$products[ $scope_key ]['Conversions'] += (float) ( $row['metrics']['conversions'] ?? 0 );
$products[ $scope_key ]['ConversionValue'] += (float) ( $row['metrics']['conversionsValue'] ?? 0 );
}
return array_values( $products );
@@ -502,6 +562,7 @@ class GoogleAdsApi
$gaql = "SELECT "
. "campaign.id, "
. "campaign.name, "
. "campaign.advertising_channel_type, "
. "campaign.bidding_strategy_type, "
. "campaign.target_roas.target_roas, "
. "campaign_budget.amount_micros, "
@@ -526,6 +587,7 @@ class GoogleAdsApi
$campaigns[ $cid ] = [
'campaign_id' => $cid,
'campaign_name' => $row['campaign']['name'] ?? '',
'advertising_channel_type' => (string) ( $row['campaign']['advertisingChannelType'] ?? '' ),
'bidding_strategy' => $row['campaign']['biddingStrategyType'] ?? 'UNKNOWN',
'target_roas' => isset( $row['campaign']['targetRoas']['targetRoas'] )
? (float) $row['campaign']['targetRoas']['targetRoas']
@@ -601,8 +663,8 @@ class GoogleAdsApi
. "metrics.conversions, "
. "metrics.conversions_value "
. "FROM ad_group "
. "WHERE campaign.status != 'REMOVED' "
. "AND ad_group.status != 'REMOVED' "
. "WHERE campaign.status = 'ENABLED' "
. "AND ad_group.status = 'ENABLED' "
. "AND segments.date DURING LAST_30_DAYS";
$results = $this -> search_stream( $customer_id, $gaql );
@@ -623,8 +685,8 @@ class GoogleAdsApi
. "metrics.conversions, "
. "metrics.conversions_value "
. "FROM ad_group "
. "WHERE campaign.status != 'REMOVED' "
. "AND ad_group.status != 'REMOVED'";
. "WHERE campaign.status = 'ENABLED' "
. "AND ad_group.status = 'ENABLED'";
$results = $this -> search_stream( $customer_id, $gaql );
if ( $results === false ) return false;
@@ -653,7 +715,15 @@ class GoogleAdsApi
$results = $this -> search_stream( $customer_id, $gaql );
if ( $results === false ) return false;
return $this -> aggregate_search_terms( $results );
$terms = $this -> aggregate_search_terms( $results );
$pmax_terms = $this -> get_pmax_search_terms_30_days( $customer_id );
if ( $pmax_terms !== false && is_array( $pmax_terms ) && !empty( $pmax_terms ) )
{
$terms = array_merge( $terms, $pmax_terms );
}
return $terms;
}
public function get_search_terms_all_time( $customer_id )
@@ -676,7 +746,58 @@ class GoogleAdsApi
$results = $this -> search_stream( $customer_id, $gaql );
if ( $results === false ) return false;
return $this -> aggregate_search_terms( $results );
$terms = $this -> aggregate_search_terms( $results );
$pmax_terms = $this -> get_pmax_search_terms_all_time( $customer_id );
if ( $pmax_terms !== false && is_array( $pmax_terms ) && !empty( $pmax_terms ) )
{
$terms = array_merge( $terms, $pmax_terms );
}
return $terms;
}
private function get_pmax_search_terms_30_days( $customer_id )
{
$gaql = "SELECT "
. "campaign.id, "
. "campaign_search_term_view.search_term, "
. "metrics.impressions, "
. "metrics.clicks, "
. "metrics.cost_micros, "
. "metrics.conversions, "
. "metrics.conversions_value "
. "FROM campaign_search_term_view "
. "WHERE campaign.status != 'REMOVED' "
. "AND campaign.advertising_channel_type = 'PERFORMANCE_MAX' "
. "AND metrics.clicks > 0 "
. "AND segments.date DURING LAST_30_DAYS";
$results = $this -> search_stream( $customer_id, $gaql );
if ( $results === false ) return false;
return $this -> aggregate_campaign_search_terms( $results );
}
private function get_pmax_search_terms_all_time( $customer_id )
{
$gaql = "SELECT "
. "campaign.id, "
. "campaign_search_term_view.search_term, "
. "metrics.impressions, "
. "metrics.clicks, "
. "metrics.cost_micros, "
. "metrics.conversions, "
. "metrics.conversions_value "
. "FROM campaign_search_term_view "
. "WHERE campaign.status != 'REMOVED' "
. "AND campaign.advertising_channel_type = 'PERFORMANCE_MAX' "
. "AND metrics.clicks > 0";
$results = $this -> search_stream( $customer_id, $gaql );
if ( $results === false ) return false;
return $this -> aggregate_campaign_search_terms( $results );
}
public function get_negative_keywords( $customer_id )
@@ -871,4 +992,59 @@ class GoogleAdsApi
return array_values( $terms );
}
private function aggregate_campaign_search_terms( $results )
{
$terms = [];
foreach ( $results as $row )
{
$campaign_id = $row['campaign']['id'] ?? null;
$search_term = trim( (string) ( $row['campaignSearchTermView']['searchTerm'] ?? '' ) );
if ( !$campaign_id || $search_term === '' )
{
continue;
}
$key = $campaign_id . '|0|' . strtolower( $search_term );
if ( !isset( $terms[ $key ] ) )
{
$terms[ $key ] = [
'campaign_id' => (int) $campaign_id,
'ad_group_id' => 0,
'ad_group_name' => 'PMax (bez grup reklam)',
'search_term' => $search_term,
'impressions' => 0,
'clicks' => 0,
'cost' => 0.0,
'conversions' => 0.0,
'conversion_value' => 0.0,
'roas' => 0.0
];
}
$terms[ $key ]['impressions'] += (int) ( $row['metrics']['impressions'] ?? 0 );
$terms[ $key ]['clicks'] += (int) ( $row['metrics']['clicks'] ?? 0 );
$terms[ $key ]['cost'] += (float) ( $row['metrics']['costMicros'] ?? 0 ) / 1000000;
$terms[ $key ]['conversions'] += (float) ( $row['metrics']['conversions'] ?? 0 );
$terms[ $key ]['conversion_value'] += (float) ( $row['metrics']['conversionsValue'] ?? 0 );
}
foreach ( $terms as $key => &$term )
{
if ( (int) $term['clicks'] <= 0 )
{
unset( $terms[ $key ] );
continue;
}
$term['roas'] = ( $term['cost'] > 0 )
? round( ( $term['conversion_value'] / $term['cost'] ) * 100, 2 )
: 0;
}
return array_values( $terms );
}
}