0% found this document useful (0 votes)
126 views31 pages

Message

Uploaded by

Shariar Mozumder
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
126 views31 pages

Message

Uploaded by

Shariar Mozumder
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd

{

"_id" : ObjectId("68b9c28311c19ccd77636009"),
"provider" : "bitbucket",
"prId" : "63",
"prUser" : "Tanjin Alam",
"prUserAvatar" : "https://avatar-management--avatars.us-west-2.prod.public.atl-
paas.net/5f9e984d62584c006b00d5f1/11bb4f36-faa6-4ac8-9c8d-5045b820727c/128",
"owner" : "inovixpro",
"repo" : "test",
"prNumber" : "63",
"prUrl" : "https://bitbucket.org/inovixpro/test/pull-requests/63",
"installationId" : "bitbucket_integration",
"prRepoName" : "inovixpro/test",
"prTitle" : "FacebookAdServiceOld.php edited online with Bitbucket",
"prBody" : "FacebookAdServiceOld.php edited online with Bitbucket",
"prState" : "open",
"prCreatedAt" : "2025-09-04T16:46:53.148216+00:00",
"prUpdatedAt" : "2025-09-04T16:46:53.617436+00:00",
"prClosedAt" : "",
"prMergedAt" : "",
"prHeadBranch" : "Tanjin-Alam/facebookadserviceoldphp-edited-online-wi-
1757004408219",
"prBaseBranch" : "main",
"prHeadSha" : "58301c86eda4",
"prBaseSha" : "ff67b91d0a1d",
"prFilesChanged" : NumberInt(1),
"prTotalLineAddition" : NumberInt(1),
"prTotalLineDeletion" : NumberInt(0),
"prFiles" : [
{
"prFileName" : "FacebookAdServiceOld.php",
"prFileStatus" : "modified",
"prFileAdditions" : NumberInt(1),
"prFileDeletions" : NumberInt(0),
"prFileChanges" : NumberInt(1),
"prFileContentBefore" : "<?php\n\nnamespace App\\Services;\n\nuse
Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Arr;\nuse App\\Models\\
FacebookAdData;\nuse Illuminate\\Support\\Facades\\Log;\nuse App\\Models\\
FacebookAdDataAggregated;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse
App\\Models\\UserFacebookAccount;\nuse App\\Exceptions\\FacebookApiException;\nuse
Exception;\nuse FacebookAds\\Api;\nuse FacebookAds\\Logger\\CurlLogger;\nuse
GuzzleHttp\\Client;\nuse Illuminate\\Support\\Carbon;\nuse GuzzleHttp\\Exception\\
GuzzleException;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Psr\\
Container\\ContainerExceptionInterface;\nuse Psr\\Container\\
NotFoundExceptionInterface;\n\nclass FacebookAdService\n{\n protected string
$appId;\n\n protected string $appSecret;\n\n protected string $apiVersion;\n\
n protected string $redirectUri;\n\n /**\n * FacebookAdService
constructor.\n * @throws FacebookApiException\n */\n public function
__construct()\n {\n $this->appId =
config('services.facebook.business_client_id');\n $this->appSecret =
config('services.facebook.business_client_secret');\n $this->apiVersion =
config('services.facebook.api_version');\n $this->redirectUri =
config('services.facebook.business_redirect');\n\n if (empty($this->appId)
|| empty($this->appSecret)) {\n Log::error('Facebook app credentials not
set in configuration');\n throw new FacebookApiException('Facebook app
credentials are not properly configured');\n }\n }\n\n /**\n * Get
the login URL for Facebook OAuth.\n *\n * @return string\n * @throws
FacebookApiException\n */\n public function getLoginUrl(): string\n {\n
try {\n // Define the permissions we need\n $permissions = [\
n 'email',\n 'public_profile',\n
'ads_management',\n 'ads_read',\n
'business_management',\n 'pages_read_engagement',\n
'pages_show_list',\n ];\n\n // Generate state parameter for
CSRF protection\n $state = bin2hex(random_bytes(16));\n
session(['fb_state' => $state]);\n\n // Build the login URL manually\n
$params = [\n 'client_id' => $this->appId,\n
'redirect_uri' => $this->redirectUri,\n 'state' => $state,\n
'response_type' => 'code',\n 'scope' => implode(',', $permissions),\
n ];\n\n return 'https://www.facebook.com/' . $this-
>apiVersion . '/dialog/oauth?' . http_build_query($params);\n } catch
(Exception $e) {\n Log::error('Facebook login URL error: ' . $e-
>getMessage());\n throw new FacebookApiException('Failed to generate
Facebook login URL: ' . $e->getMessage());\n }\n }\n\n /**\n *
Handle the callback from Facebook OAuth.\n *\n * @param string $code The
authorization code from Facebook\n * @return array Access token details\n *
@throws FacebookApiException\n */\n public function handleCallback(string
$code): array\n {\n try {\n // Verify state parameter to
prevent CSRF attacks\n $state = request()->get('state');\n
$sessionState = session('fb_state');\n\n if (!$state || !$sessionState
|| $state !== $sessionState) {\n throw new
FacebookApiException('Invalid state parameter, possible CSRF attack');\n
}\n\n // Clear the state from session\n session()-
>forget('fb_state');\n\n // Exchange the authorization code for an
access token\n $client = new Client();\n $response = $client-
>post('https://graph.facebook.com/' . $this->apiVersion . '/oauth/access_token', [\
n 'form_params' => [\n 'client_id' => $this-
>appId,\n 'client_secret' => $this->appSecret,\n
'redirect_uri' => $this->redirectUri,\n 'code' => $code,\n
],\n ]);\n\n $data = json_decode((string)$response-
>getBody(), true);\n\n if (!isset($data['access_token'])) {\n
throw new FacebookApiException('Failed to obtain access token.');\n }\n\
n // Get the short-lived access token\n $accessToken =
$data['access_token'];\n\n // Exchange for a long-lived token\n
$response = $client->get('https://graph.facebook.com/' . $this->apiVersion .
'/oauth/access_token', [\n 'query' => [\n
'grant_type' => 'fb_exchange_token',\n 'client_id' => $this-
>appId,\n 'client_secret' => $this->appSecret,\n
'fb_exchange_token' => $accessToken,\n ],\n ]);\n\n
$data = json_decode((string)$response->getBody(), true);\n\n if (!
isset($data['access_token'])) {\n throw new
FacebookApiException('Failed to obtain long-lived access token.');\n }\
n\n $longLivedToken = $data['access_token'];\n $expiresIn =
$data['expires_in'] ?? 5184000; // Default to 60 days if not provided\n\n
// Get token debug info\n $response =
$client->get('https://graph.facebook.com/' . $this->apiVersion . '/debug_token', [\
n 'query' => [\n 'input_token' =>
$longLivedToken,\n 'access_token' => $this->appId . '|' . $this-
>appSecret,\n ],\n ]);\n\n $debugData =
json_decode((string)$response->getBody(), true);\n\n if (!
isset($debugData['data']['user_id'])) {\n throw new
FacebookApiException('Failed to get user ID from token.');\n }\n\n
// Calculate token expiration date\n $expiresAt = Carbon::now()-
>addSeconds($expiresIn);\n\n // Return token details and ad account ID\n
return [\n 'access_token' => $longLivedToken,\n
'expires_at' => $expiresAt,\n 'user_id' => $debugData['data']
['user_id'],\n ];\n } catch (GuzzleException|
NotFoundExceptionInterface|ContainerExceptionInterface $e) {\n
Log::error('Facebook API request error: ' . $e->getMessage());\n throw
new FacebookApiException('Error making request to Facebook: ' . $e->getMessage());\
n } catch (Exception $e) {\n if (!$e instanceof
FacebookApiException) {\n Log::error('Facebook token exchange error:
' . $e->getMessage());\n throw new FacebookApiException('Error
exchanging Facebook code for token: ' . $e->getMessage());\n }\n
throw $e;\n }\n }\n\n /**\n * Save the Facebook access token for a
user.\n *\n * @param Authenticatable $user\n * @param array
$tokenDetails\n * @return UserFacebookAccount\n */\n public function
saveUserToken(Authenticatable $user, array $tokenDetails): UserFacebookAccount\n
{\n $currentWorkspaceId = $user->current_team_id;\n\n $userData = [\n
'team_id' => $currentWorkspaceId,\n 'access_token' =>
$tokenDetails['access_token'],\n 'token_expires_at' =>
$tokenDetails['expires_at'],\n 'account_status' =>
UserFacebookAccount::STATUS_ACTIVE,\n ];\n\n // Also save the
ad_account_id if it's included in the token details\n if
(isset($tokenDetails['ad_account_id'])) {\n $userData['ad_account_id'] =
$tokenDetails['ad_account_id'];\n }\n\n return
UserFacebookAccount::updateOrCreate(\n ['user_id' =>
WorkspaceService::getWorkspaceUser()?->id, 'team_id' => $currentWorkspaceId],\n
$userData\n );\n }\n\n /**\n * Get the user's ad accounts from
Facebook.\n *\n * @param Authenticatable $user\n * @param string|null
$businessId Optional business ID to use a specific account\n * @return array\n
* @throws FacebookApiException\n */\n public function
getAdAccounts(Authenticatable $user, ?string $businessId = null): array\n {\n
try {\n $account = $this->getUserFacebookAccount($user, $businessId);\n\
n // Initialize the Facebook Ads API with the user's access token\n
$api = Api::init(null, null, $account->access_token);\n\n // Enable
debugging if needed\n if (config('app.debug')) {\n $api-
>setLogger(new CurlLogger());\n }\n\n // Get user's ad
accounts using the /me/adaccounts endpoint\n $client = new Client();\n
$response = $client->get('https://graph.facebook.com/' . $this->apiVersion .
'/me/adaccounts', [\n 'query' => [\n
'access_token' => $account->access_token,\n 'fields' =>
'id,name,account_id,account_status,amount_spent,business_name,currency,timezone_nam
e',\n ],\n ]);\n\n $data =
json_decode((string)$response->getBody(), true);\n\n if (!
isset($data['data'])) {\n throw new FacebookApiException('Failed to
fetch ad accounts data');\n }\n\n // Update last fetch time\n
$account->last_data_fetch = now();\n $account->save();\n\n
return $data['data'];\n } catch (GuzzleException $e) {\n
Log::error('Facebook API request error: ' . $e->getMessage());\n $this-
>handleApiError($e, $user);\n throw new FacebookApiException('Error
connecting to Facebook API: ' . $e->getMessage());\n } catch (Exception $e)
{\n if (!$e
instanceof FacebookApiException) {\n Log::error('Facebook ad
accounts error: ' . $e->getMessage());\n throw new
FacebookApiException('Error fetching Facebook ad accounts: ' . $e->getMessage());\n
}\n throw $e;\n }\n }\n\n /**\n * Get campaigns for a
specific ad account.\n *\n * @param Authenticatable $user\n * @param
string $adAccountId\n * @param string|null $businessId Optional business ID to
use a specific account\n * @return array\n * @throws FacebookApiException\n
*/\n public function getCampaigns(Authenticatable $user, string $adAccountId, ?
string $businessId = null): array\n {\n try {\n $account =
$this->getUserFacebookAccount($user, $businessId, $adAccountId);\n\n //
Initialize the Facebook Ads API with the user's access token\n $api =
Api::init(null, null, $account->access_token);\n\n // Enable debugging
if needed\n if (config('app.debug')) {\n $api-
>setLogger(new CurlLogger());\n }\n\n // Get campaigns for
the specified ad account\n $client = new Client();\n
$response = $client->get('https://graph.facebook.com/' . $this->apiVersion . '/' .
$account->ad_account_id . '/campaigns', [\n 'query' => [\n
'access_token' => $account->access_token,\n 'fields' =>
'id,name,objective,status,spend_cap,budget_remaining,daily_budget,lifetime_budget,s
tart_time,stop_time,updated_time,created_time',\n 'limit' =>
100,\n ],\n ]);\n\n $data =
json_decode((string)$response->getBody(), true);\n\n if (!
isset($data['data'])) {\n throw new FacebookApiException('Failed to
fetch campaign data');\n }\n\n return $data['data'];\n
} catch (GuzzleException $e) {\n Log::error('Facebook API request error:
' . $e->getMessage());\n $this->handleApiError($e, $user);\n
throw new FacebookApiException('Error connecting to Facebook API: ' . $e-
>getMessage());\n } catch (Exception $e) {\n if (!$e instanceof
FacebookApiException) {\n Log::error('Facebook campaigns error: ' .
$e->getMessage());\n throw new FacebookApiException('Error fetching
Facebook campaigns: ' . $e->getMessage());\n }\n throw $e;\n
}\n }\n\n /**\n * Disconnect a Facebook business account for a user.\n
*\n * @param Authenticatable $user\n * @param string|null $businessId
Optional business ID to disconnect a specific account\n * @return bool\n
*/\n public function disconnectAccount(Authenticatable $user, ?string
$businessId = null): bool\n {\n $currentWorkspaceId = $user-
>current_team_id;\n\n $query = UserFacebookAccount::where('team_id',
$currentWorkspaceId)\n ->where('user_id', $user->id)\n -
>where('account_status', UserFacebookAccount::STATUS_ACTIVE);\n\n // If a
specific business ID is provided, disconnect only that account\n if
($businessId) {\n $query->where('business_id', $businessId);\n }\
n\n $accounts = $query->get();\n\n if ($accounts->isNotEmpty()) {\n
foreach ($accounts as $account) {\n $account->account_status =
UserFacebookAccount::STATUS_DISCONNECTED;\n $account->save();\n
}\n return true;\n }\n\n return false;\n }\n\n /**\n
* Check if the user has a valid Facebook connection.\n *\n * @return bool\n
*/\n public function isConnected(): bool\n {\n $user =
WorkspaceService::getWorkspaceUser();\n $currentWorkspaceId = $user-
>current_team_id;\n\n return UserFacebookAccount::where('user_id', $user?-
>id)\n ->where('team_id', $currentWorkspaceId)\n -
>where('account_status', UserFacebookAccount::STATUS_ACTIVE)\n -
>whereNotNull([\n 'access_token',\n 'business_id',\n
'ad_account_id',\n 'optimization',\n 'roas_target',\n
'spend_target',\n ])\n ->exists();\n }\n\n /**\n *
Get the user's business accounts from Facebook.\n *\n * @param
Authenticatable $user\n * @param string|null $businessId Optional business ID
to use a specific account\n * @return array\n * @throws
FacebookApiException|GuzzleException\n */\n public function
getBusinessAccounts(Authenticatable $user, ?string $businessId = null): array\n
{\n try {\n $account = $this->getUserFacebookAccount($user,
$businessId);\n\n // Make a GET request to fetch business accounts with
profile pictures\n $client = new Client();\n $response =
$client->get('https://graph.facebook.com/' . $this->apiVersion . '/me/businesses',
[\n 'query' => [\n 'access_token' => $account-
>access_token,\n 'fields' =>
'id,name,verification_status,picture{url,width,height}',\n ],\n
]);\n\n $data = json_decode((string)$response->getBody(), true);\n\n
return $data['data'] ?? [];\n } catch (Exception $e) {\n
Log::error('Error fetching Facebook business accounts: ' . $e->getMessage());\n
throw new FacebookApiException('Failed to retrieve business accounts: ' . $e-
>getMessage());\n }\n }\n\n /**\n * Update or create a Facebook
business account for a user.\n * This method supports the multi-business
account feature by creating separate\n * account records for each business ID.\
n *\n * @param Authenticatable $user\n * @param string $businessId The
business ID to connect\n * @param string|null $businessName Optional business
name\n * @return UserFacebookAccount\n * @throws FacebookApiException\n
*/\n public function updateBusinessId(Authenticatable $user, string $businessId,
?string $businessName = null): UserFacebookAccount\n {\n try {\n
$currentWorkspaceId = $user->current_team_id;\n\n // First check if we
already have an account with this specific business_id\n $account =
UserFacebookAccount::where([\n 'team_id' => $currentWorkspaceId,\n
'user_id' => $user->id,\n 'business_id' => $businessId,\n
'ad_account_id' => null\n ])->first();\n\n if (!$account) {\n
// No existing account with this business_id, create a new one\n
$account = new UserFacebookAccount([\n 'team_id' =>
$currentWorkspaceId,\n 'user_id' => $user->id,\n
'business_id' => $businessId,\n 'business_name' =>
$businessName,\n ]);\n\n // Get token information
from another active account if available\n $existingAccount =
UserFacebookAccount::where('team_id', $currentWorkspaceId)\n -
>where('account_status', UserFacebookAccount::STATUS_ACTIVE)\n -
>first();\n\n if ($existingAccount) {\n $account-
>access_token = $existingAccount->access_token;\n $account-
>token_expires_at = $existingAccount->token_expires_at;\n
$account->refresh_token = $existingAccount->refresh_token;\n }\n
} else {\n // We found an existing account with this business_id,
update it\n if ($businessName) {\n $account-
>business_name = $businessName;\n }\n }\n\n //
Make sure the account is active\n $account->account_status =
UserFacebookAccount::STATUS_ACTIVE;\n $account->save();\n\n
return $account;\n } catch (Exception $e) {\n Log::error('Error
updating Facebook business ID: ' . $e->getMessage());\n throw new
FacebookApiException('Failed to update business ID: ' . $e->getMessage());\n
}\n }\n\n /**\n * Update the ad account ID for a specific Facebook
business account.\n *\n * @param Authenticatable $user\n * @param
string $adAccountId\n * @param string|null $businessId Optional specific
business ID to update\n * @param string|null $adAccountName\n * @return
UserFacebookAccount\n * @throws FacebookApiException\n */\n public
function updateAdAccountId(Authenticatable $user, string $adAccountId, ?string
$businessId = null, ?string $adAccountName = null): UserFacebookAccount\n {\n
try {\n $account = $this->getUserFacebookAccount($user, $businessId);\n\
n // Format the ad account ID if needed\n if (!
str_starts_with($adAccountId, 'act_')) {\n $adAccountId = 'act_' .
$adAccountId;\n }\n\n // Update the account with the ad
account ID\n $account->ad_account_id = $adAccountId;\n
$account->ad_account_name = $adAccountName;\n $account->save();\n\n
return $account;\n } catch (Exception $e) {\n Log::error('Error
updating Facebook ad account ID: ' . $e->getMessage());\n throw new
FacebookApiException('Failed to update ad account ID: ' . $e->getMessage());\n
}\n }\n\n /**\n * Get comprehensive ad insights with performance
metrics.\n *\n * @param Authenticatable $user\n * @param string
$adAccountId\n * @param array|string $dateRange\n * @param array $metrics\n
* @param string|null $businessId Optional business ID to use a specific
account\n * @return array\n * @throws FacebookApiException\n * @throws
GuzzleException\n */\n public function getAdInsights(Authenticatable $user,
string $adAccountId, array|string $dateRange, array $metrics = [], ?string
$businessId = null): array\n {\n try {\n $account = $this-
>getUserFacebookAccount($user, $businessId, $adAccountId);\n\n //
Default metrics if not specified\n if (empty($metrics)) {\n
$metrics = [\n 'impressions',\n 'reach',\n
'clicks',\n 'spend',\n 'ctr',\n
'cpm',\n 'cpp',\n 'purchase_roas',\n
'cost_per_inline_link_click',\n 'cost_per_action_type',\n
'actions',\n 'action_values',\n // Video
metrics for Hook Rate and Hold Rate\n
'video_p25_watched_actions',\n 'video_p50_watched_actions',\n
'video_p75_watched_actions',\n
'video_avg_time_watched_actions',\n 'video_play_actions',\n
//'video_play_curve_actions',\n ];\n }\n\n
$creativeFields = implode(',', [\n 'id',\n 'name',\n
'thumbnail_url',\n 'image_url',\n //'video_id',\n
//'effective_object_story_id',\
n //'object_story_spec{link_data{message,call_to_action{type,value}}
}'\n ]);\n\n $timeRange = $this->parseDateRange($dateRange);\
n\n // Get ads with their creatives and insights\n $client =
new Client();\n $response =
$client->get(\"https://graph.facebook.com/$this->apiVersion/$account-
>ad_account_id/ads\", [\n 'query' => [\n
'access_token' => $account->access_token,\n 'fields' =>
'id,name,status,adset{id,name,campaign{id,name}},creative{' . $creativeFields .
'},insights.time_range(' . json_encode($timeRange) . '){' . implode(',',
$metrics) . '}',\n 'limit' => 250\n ],\n
]);\n\n $data = json_decode((string)$response->getBody(), true);\n\n
if (!Arr::exists($data, 'data') || empty($data['data'])) {\n return
[];\n }\n\n $creativeIds = [];\n foreach
($data['data'] as $ad) {\n if ($cid = data_get($ad, 'creative.id'))
{\n $creativeIds[$cid] = true;\n }\
n }\n // Batch-fetch thumbnail_url at 600 px\n
foreach (array_chunk(array_keys($creativeIds), 50) as $chunk) {\n
$resp = $client->get(\"https://graph.facebook.com/$this->apiVersion/\", [\n
'query' => [\n 'ids' => implode(',', $chunk),\n
'fields' => 'thumbnail_url',\n 'thumbnail_width' => 600,\n
'thumbnail_height' => 600,\n 'access_token' => $account-
>access_token,\n ],\n ]);\n
$thumbs = json_decode($resp->getBody(), true);\n foreach
($data['data'] as &$ad) {\n $cid = $ad['creative']['id'] ??
null;\n if ($cid && isset($thumbs[$cid]['thumbnail_url'])) {\n
$ad['creative']['thumbnail_url_hd'] = $thumbs[$cid]['thumbnail_url'];\n
}\n }\n }\n\n /*// Create a map from ad ID to
video ID\n $adToVideoMap = collect($data['data'])->mapWithKeys(function
($item) {\n return [$item['id'] => data_get($item,
'creative.video_id')];\n })->filter()->unique()->toArray();\n
$allVideos = [];\n $videoChunks = array_chunk($adToVideoMap, 25, true);\
n // Fetch video details for all identified video IDs\n
foreach ($videoChunks as $chunk) {\n $response = $client-
>get(\"https://graph.facebook.com/$this->apiVersion/$adAccountId/advideos\", [\n
'query' => [\n 'video_ids' => implode(',', $chunk),\n
'fields' => 'id,source,length,thumbnails{uri,is_preferred,width,height}',\n
'access_token' => $account->access_token,\n ],\
n ]);\n $payload = json_decode((string)$response-
>getBody(), true);\n if (!empty($payload) &&
isset($payload['data'])) {\n $allVideos[] = $payload['data'];\n
}\n }\n return $allVideos;*/\n\n // Process and
structure the data\n $processedAds = [];\n foreach
($data['data'] as $ad) {\n $insights = $ad['insights']['data'][0] ??
[];\n\n // Get all ad details\n $adDetails = [\n
'id' => $ad['id'],\n 'name' => $ad['name'],\n
'status' => $ad['status'],\n 'adset' => $ad['adset']['name'] ??
'',\n 'adset_id' => $ad['adset']['id'] ?? '',\n
'campaign' => $ad['adset']['campaign']['name'] ?? '',\n
'campaign_id' => $ad['adset']['campaign']['id'] ?? '',\n
'thumbnail' => $ad['creative']['thumbnail_url'] ?? '',\n
'creative_id' => $ad['creative']['id'] ?? '',\n 'video_id' =>
$ad['creative']['video_id'] ?? '',\n 'message' =>
$ad['creative']['object_story_spec']['link_data']['message'] ?? $ad['creative']
['object_story_spec']['video_data']['message'] ?? '',\n
'call_to_action' => $ad['creative']['object_story_spec']['link_data']
['call_to_action']['type'] ?? $ad['creative']['object_story_spec']['video_data']
['call_to_action']['type'] ?? '',\n 'call_to_action_value' =>
$ad['creative']['object_story_spec']['link_data']['call_to_action']['value']
['link'] ?? $ad['creative']['object_story_spec']['video_data']['call_to_action']
['value']['link'] ?? '',\n 'effective_object_story_id' =>
$ad['creative']['effective_object_story_id'] ?? '',\n
'original_image_url' => $ad['creative']['thumbnail_url_hd'] ?? '',\n
];\n\n // Directly add metrics to the ad details with null-safe
access\n $adDetails['metrics'] = [\n
'impressions' => $insights['impressions'] ?? 0,\n 'reach' =>
$insights['reach'] ?? 0,\n 'clicks' => $insights['clicks'] ??
0,\n 'spend' => $insights['spend'] ?? 0,\n
'ctr' => $insights['ctr'] ?? 0,\n 'cpm' => $insights['cpm'] ??
0,\n 'cpp' => $insights['cpp'] ?? 0,\n
'cost_per_inline_link_click' => $insights['cost_per_inline_link_click'] ?? 0,\n
'cost_per_action_type' => $insights['cost_per_action_type'] ?? [],\n
'actions' => $insights['actions'] ?? [],\n 'action_values' =>
$insights['action_values'] ?? [],\n // Extract numeric values
directly from video metrics arrays with clearer names\n
'p25_video_views' => $this-
>extractVideoMetricValue($insights['video_p25_watched_actions'] ?? []),\n
'p50_video_views' => $this-
>extractVideoMetricValue($insights['video_p50_watched_actions'] ?? []),\n
'p75_video_views' => $this-
>extractVideoMetricValue($insights['video_p75_watched_actions'] ?? []),\n
'avg_watch_time' => $this-
>extractVideoMetricValue($insights['video_avg_time_watched_actions'] ?? []),\n
'play_actions' => $this->extractVideoMetricValue($insights['video_play_actions'] ??
[]),\n ];\n\n $spend = (float)($insights['spend'] ??
0);\n\n $cpa = 0;\n $revenue = 0;\n
$purchasesTotal = 0;\n $conversionRate = 0;\n
$totalConversions = 0;\n\n // Count total conversions from actions\n
if (!empty($insights['actions']) && is_array($insights['actions'])) {\n
// Meaningful conversion actions\n $conversionTypes = [\n
'omni_purchase',\n 'lead',\n
'complete_registration',\n 'subscribe',\n
'add_to_cart',\n 'initiate_checkout',\n
'add_payment_info',\n 'start_trial',\n
'add_to_wishlist',\n ];\n\n foreach
($insights['actions'] as $action) {\n if
(in_array($action['action_type'] ?? '', $conversionTypes)) {\n
$totalConversions += (int)($action['value'] ?? 0);\n }\n\n
// Extract purchase actions specifically for revenue\n if
(($action['action_type'] ?? '') === 'omni_purchase') {\n
$purchasesTotal += (float)($action['value'] ?? 0);\n }\n
}\n\n // Calculate CPA if there are conversions and spend\n
if ($totalConversions > 0 && $spend > 0) {\n $cpa = $spend /
$totalConversions;\n }\n\n // Calculate
conversion rate (as a percentage)\n $impressions = (int)
($insights['impressions']
?? 0);\n if ($impressions > 0) {\n
$conversionRate = ($totalConversions / $impressions) * 100;\n }\
n }\n\n /*$purchaseActionTypes = [\n
'purchase',\n 'offsite_conversion.fb_pixel_purchase',\n
'onsite_web_purchase',\n 'onsite_web_app_purchase',\n
'omni_purchase',\n 'web_in_store_purchase',\n
'app_custom_event.fb_mobile_purchase'\n ];*/\n\n //
Calculate Revenue from action_values\n if (!
empty($insights['action_values']) && is_array($insights['action_values'])) {\n
foreach ($insights['action_values'] as $actionValue) {\n if
($actionValue['action_type'] === 'omni_purchase') {\n
$revenue = (float)($actionValue['value'] ?? 0);\n }\n
}\n }\n\n $roas = $spend > 0 ? $revenue / $spend :
0;\n $profitLoss = $revenue - $spend;\n\n // Add all
metrics to the response\n $adDetails['metrics']['roas'] =
round($roas, 2);\n $adDetails['metrics']['cpa'] = round($cpa, 2);\n
$adDetails['metrics']['conversion_rate'] = round($conversionRate, 2);\n
$adDetails['metrics']['revenue'] = round($revenue, 2);\n
$adDetails['metrics']['profit_loss'] = round($profitLoss, 2);\n
$adDetails['metrics']['purchases'] = $purchasesTotal;\n\n // Extract
all video metrics values from Facebook's available metrics\n
$videoP25Views = $this-
>extractVideoMetricValue($insights['video_p25_watched_actions'] ?? []);\n
$videoP50Views = $this-
>extractVideoMetricValue($insights['video_p50_watched_actions'] ?? []);\n
$videoP75Views = $this-
>extractVideoMetricValue($insights['video_p75_watched_actions'] ?? []);\n
$videoAvgTimeWatched = $this-
>extractVideoMetricValue($insights['video_avg_time_watched_actions'] ?? []);\n
$videoPlayActions = $this-
>extractVideoMetricValue($insights['video_play_actions'] ?? []);\n\n
// Extract 3-second video views from the actions array (video_view action type)\n
$video3SecViews = 0;\n if (!empty($insights['actions']) &&
is_array($insights['actions'])) {\n foreach
($insights['actions'] as $action) {\n if
(($action['action_type'] ?? '') === 'video_view') {\n
$video3SecViews = (int)($action['value'] ?? 0);\n
break;\n }\n }\n }\n\n
// Calculate Hook Rate: (3-second video views / total impressions) * 100\n
$hookRate = 0;\n if ($adDetails['metrics']['impressions'] > 0 &&
$video3SecViews > 0) {\n $hookRate = ($video3SecViews /
$adDetails['metrics']['impressions']) * 100;\n }\n\
n // Calculate Hold Rate: (P75 views / 3-second video views) * 100\n
// This is a more reliable indicator of viewers who watched a significant portion
of the video\n $holdRate = 0;\n if ($video3SecViews >
0 && $videoP75Views > 0) {\n $holdRate = ($videoP75Views /
$video3SecViews) * 100;\n }\n\n // Calculate Cost per
3-Second View: Total ad spend / Number of 3-second video views\n
$costPer3SecView = 0;\n if ($video3SecViews > 0 && $spend > 0) {\n
$costPer3SecView = $spend / $video3SecViews;\n }\n\
n // Handle edge cases - cap rates to reasonable limits\n
if ($hookRate > 100) $hookRate = 100; // Hook rate can't exceed 100%\n
if ($holdRate > 100) $holdRate = 100; // Hold rate can't exceed 100%\n
if ($costPer3SecView < 0) $costPer3SecView = 0; // Cost can't be negative\n\n
// Check for Conversion Rate Consistency\n
$conversionRateWarningFlag = $this->isConversionRateWarningFlag($hookRate,
$holdRate, $conversionRate);\n\n // Store all raw video metrics data
for future reference and analysis\n $videoMetricsData = [\n
// Raw metrics data from Facebook API\n 'raw_data' => [\n
'video_p25_watched_actions' => $insights['video_p25_watched_actions'] ?? [],\n
'video_p50_watched_actions' => $insights['video_p50_watched_actions'] ?? [],\n
'video_p75_watched_actions' => $insights['video_p75_watched_actions'] ?? [],\n
'video_avg_time_watched_actions' => $insights['video_avg_time_watched_actions'] ??
[],\n 'video_play_actions' =>
$insights['video_play_actions'] ?? [],\n
'video_play_curve_actions' => $insights['video_play_curve_actions'] ?? []\n
],\n // Clean extracted values with consistent naming\n
'metrics' => [\n // Basic video view metrics\n
'video_3sec_views' => $video3SecViews, // From video_view action type\n
'video_p25_views' => $videoP25Views,\n 'video_p50_views' =>
$videoP50Views,\n 'video_p75_views' => $videoP75Views,\n
'avg_watch_time' => $videoAvgTimeWatched,\n 'play_actions'
=> $videoPlayActions,\n\n // Calculated performance metrics\
n 'hook_rate' => round($hookRate, 2),\n
'hold_rate' => round($holdRate, 2),\n 'cost_per_3sec_view'
=> round($costPer3SecView, 2)\n ]\n ];\n\n
// Add all video metrics to the metrics array with consistent names\n
$adDetails['metrics']['video_3sec_views'] = $video3SecViews; // From video_view
action type\n $adDetails['metrics']['video_p25_views'] =
$videoP25Views;\n $adDetails['metrics']['video_p50_views'] =
$videoP50Views;\n $adDetails['metrics']['video_p75_views'] =
$videoP75Views;\n $adDetails['metrics']['video_avg_time_watched'] =
$videoAvgTimeWatched;\n $adDetails['metrics']['video_play_actions']
= $videoPlayActions;\n\n // Calculated performance metrics\n
$adDetails['metrics']['hook_rate'] = round($hookRate, 2);\n
$adDetails['metrics']['hold_rate'] = round($holdRate, 2);\n
$adDetails['metrics']['cost_per_3sec_view'] = round($costPer3SecView, 2);\n
$adDetails['metrics']['conversion_rate_warning_flag'] =
$conversionRateWarningFlag;\n $adDetails['metrics']
['video_metrics_data'] = $videoMetricsData;\n\n $processedAds[] =
$adDetails;\n }\n\n return $processedAds;\n } catch
(Exception $e) {\n Log::error('Error fetching Facebook ad insights: ' .
$e->getMessage());\n throw new FacebookApiException('Failed to retrieve
ad insights: ' . $e->getMessage());\n }\n }\n\n /**\n * Get a
user's Facebook account, optionally for a specific business or ad account.\n *\
n * @param Authenticatable $user\n * @param string|null $businessId
Optional business ID to retrieve a specific account\n * @param string|null
$adAccountId Optional ad account ID to retrieve a specific account\n * @return
UserFacebookAccount\n * @throws FacebookApiException\n */\n protected
function getUserFacebookAccount(Authenticatable $user, ?string $businessId =
null, ?string $adAccountId = null): UserFacebookAccount\n {\n
$currentWorkspaceId = $user->current_team_id;\n\n $query =
UserFacebookAccount::where('team_id', $currentWorkspaceId)\n -
>where('account_status', UserFacebookAccount::STATUS_ACTIVE);\n\n // If a
specific business ID is requested, use that\n if ($businessId) {\n
$query->where('business_id', $businessId);\n }\n\n // If a specific
ad account ID is requested, use that\n if ($adAccountId) {\n //
Format ad account ID if needed\n if (!str_starts_with($adAccountId,
'act_')) {\n $adAccountId = 'act_' . $adAccountId;\n }\n\
n $query->where('ad_account_id', $adAccountId);\n }\n\n
$account = $query->first();\n\n if (!$account) {\n $errorMessage
= $businessId\n ? \"No connected Facebook account found for business
ID: $businessId\"\n : ($adAccountId\n ? \"No
connected Facebook account found for ad account ID: $adAccountId\"\n
: 'No connected Facebook account found for this workspace');\n throw new
FacebookApiException($errorMessage);\n }\n\n return $account;\n }\
n\n /**\n * Handle Facebook API errors.\n *\n * @param Exception $e\
n * @param Authenticatable $user\n * @throws FacebookApiException\n */\
n protected function handleApiError(Exception $e, Authenticatable $user): void\n
{\n Log::error('Facebook API error: ' . $e->getMessage(), [\n
'user_id' => $user->id,\n ]);\n\n // Check if the error is due to
expired tokens\n $errorMessage = $e->getMessage();\n $errorCode = $e
instanceof GuzzleException ? 0 : $e->getCode();\n\n if ($errorCode == 190
||\n stripos($errorMessage,
'expired') !== false ||\n stripos($errorMessage, 'invalid access
token') !== false) {\n // Mark account as requiring reconnection\n
$account = UserFacebookAccount::where('team_id', $user->current_team_id)->first();\
n if ($account) {\n $account->account_status =
UserFacebookAccount::STATUS_EXPIRED;\n $account->save();\n
}\n }\n }\n\n /**\n * Get all Facebook accounts for a user's
team.\n *\n * @param Authenticatable $user\n * @return array\n */\n
public function getUserFacebookAccounts(Authenticatable $user): array\n {\n
try {\n $currentWorkspaceId = $user->current_team_id;\n\n
$accounts = UserFacebookAccount::where('team_id', $currentWorkspaceId)\n
->whereNotNull('business_id')\n ->whereNotNull('ad_account_id')\n
->where('account_status', UserFacebookAccount::STATUS_ACTIVE)\n -
>get()\n ->toArray();\n\n return [\n
'success' => true,\n 'accounts' => $accounts\n ];\n
} catch (Exception $e) {\n Log::error('Error fetching Facebook accounts:
' . $e->getMessage(), [\n 'user_id' => $user->id,\n ]);\
n\n return [\n 'success' => false,\n
'message' => 'Failed to retrieve Facebook accounts: ' . $e->getMessage()\n
];\n }\n }\n\n /**\n * Update multiple ad account IDs for a
specific business ID.\n *\n * @param Authenticatable $user\n * @param
array $adAccounts Array of ad account data with id and name\n * @param string
$businessId Optional specific business ID to update\n * @param string|null
$businessName Optional business name\n * @return array Updated accounts\n *
@throws FacebookApiException\n */\n public function
updateMultipleAdAccountIds(Authenticatable $user, array $adAccounts, string
$businessId, ?string $businessName = null): array\n {\n try {\n
$currentWorkspaceId = $user->current_team_id;\n $updatedAccounts = [];\
n\n // Begin transaction\n DB::beginTransaction();\n\n
foreach ($adAccounts as $adAccountData) {\n // Validate required
fields\n if (!isset($adAccountData['id'])) {\n
throw new FacebookApiException('Each ad account must have an ID');\n
}\n\n $adAccountId = $adAccountData['id'];\n
$adAccountName = $adAccountData['name'] ?? null;\n\n // Format the
ad account ID if needed\n if (!str_starts_with($adAccountId,
'act_')) {\n $adAccountId = 'act_' . $adAccountId;\n
}\n\n // Find existing account or create a new one\n
$account = UserFacebookAccount::firstOrNew([\n 'team_id' =>
$currentWorkspaceId,\n 'user_id' => $user->id,\n
'business_id' => $businessId,\n 'business_name' =>
$businessName,\n 'ad_account_id' => $adAccountId\n
]);\n\n // If it's a new account, set up necessary fields\n
if (!$account->exists) {\n // Try to get the access token from
an existing account\n $existingAccount =
UserFacebookAccount::where('team_id', $currentWorkspaceId)\n
->where('user_id', $user->id)\n ->where('account_status',
UserFacebookAccount::STATUS_ACTIVE)\n ->first();\n\n
// Copy relevant fields from existing account\n $account-
>access_token = $existingAccount?->access_token;\n $account-
>token_expires_at = $existingAccount?->token_expires_at;\n
$account->refresh_token = $existingAccount?->refresh_token;\n
$account->account_status = UserFacebookAccount::STATUS_ACTIVE;\n }\
n\n // Update the account with the ad account info\n
$account->ad_account_id = $adAccountId;\n if ($adAccountName) {\n
$account->ad_account_name = $adAccountName;\n }\n\n
$account->save();\n $updatedAccounts[] = $account;\n }\n\
n // Delete redundant records after multi create\n
UserFacebookAccount::where('team_id', $currentWorkspaceId)\n -
>whereNull('business_id')\n ->whereNull('business_name')\n
->where('ad_account_id', '')\n ->delete();\n\n // Commit
transaction\n DB::commit();\n\n return $updatedAccounts;\n
} catch (Exception $e) {\n // Roll back in case of error\n
DB::rollBack();\n Log::error('Error updating multiple Facebook ad
account IDs: ' . $e->getMessage());\n throw new
FacebookApiException('Failed to update multiple ad account IDs: ' . $e-
>getMessage());\n }\n }\n\n /**\n * Update optimization targets
for multiple Facebook ad accounts.\n *\n * @param Authenticatable $user\n
* @param array $adAccountIds Array of ad account IDs to update\n * @param
string $optimization Optimization strategy\n * @param float $roasTarget ROAS
target value\n * @param float $spendTarget Spend target value\n * @param
string|null $businessId Optional specific business ID\n * @return array Updated
accounts\n * @throws FacebookApiException\n */\n public function
updateMultipleOptimizationTargets(\n Authenticatable $user,\n array
$adAccountIds,\n string $optimization,\n float $roasTarget,\n
float $spendTarget,\n ?string $businessId = null\n ): array {\n
try {\n $currentWorkspaceId = $user->current_team_id;\n
$updatedAccounts = [];\n\n // Begin transaction\n
DB::beginTransaction();\n\n foreach ($adAccountIds as $adAccountId) {\n
// Format the ad account ID if needed\n if (!
str_starts_with($adAccountId, 'act_')) {\n $adAccountId = 'act_'
. $adAccountId;\n }\n\n // Find the account to
update\n $query = UserFacebookAccount::where('team_id',
$currentWorkspaceId)\n ->where('user_id', $user->id)\n
->where('account_status', UserFacebookAccount::STATUS_ACTIVE)\n
->where('ad_account_id', $adAccountId);\n\n // Filter by business ID
if provided\n if ($businessId) {\n $query-
>where('business_id', $businessId);\n }\n\n // Also
get accounts where optimization settings are NULL\n $query-
>where(function($q) {\n $q->whereNull('optimization')\n
->orWhereNull('roas_target')\n ->orWhereNull('spend_target');\
n });\n\n $accounts = $query->get();\n\n
if ($accounts->isEmpty()) {\n // Skip if no matching accounts
found\n continue;\n }\n\n foreach
($accounts as $account) {\n // Update optimization targets\n
$account->optimization = $optimization;\n $account->roas_target
= $roasTarget;\n $account->spend_target = $spendTarget;\n
$account->save();\n\n $updatedAccounts[] = $account;\n
}\n }\n\n // Commit transaction\n DB::commit();\n\
n return $updatedAccounts;\n } catch (Exception $e) {\n
// Roll back in case of error\n DB::rollBack();\n
Log::error('Error updating multiple Facebook optimization targets: ' . $e-
>getMessage());\n throw new FacebookApiException('Failed to update
multiple optimization targets: ' . $e->getMessage());\n }\n }\n\n /**\
n * Get ad accounts owned by the user's business (from the Facebook Business
API).\n * If business_id is provided, it will fetch accounts for that specific
business.\n *\n * @param Authenticatable $user\n * @param string
$businessId\n * @return array\n * @throws FacebookApiException\n */\n
public function getOwnedAdAccounts(Authenticatable $user, string $businessId):
array\n {\n try {\n $account = $this-
>getUserFacebookAccount($user);\n\n // Make a GET request to fetch owned
ad accounts for the business\n $client = new Client();\n
$response = $client->get('https://graph.facebook.com/' . $this->apiVersion . '/' .
$businessId . '/owned_ad_accounts', [\n 'query' => [\n
'access_token' => $account->access_token,\n 'fields' =>
'id,name,account_id,account_status,currency,timezone_name',\n ],\n
]);\n\n $data = json_decode((string)$response->getBody(), true);\n\n
if (!isset($data['data'])) {\n return [];\n }\n\n
return $data['data'];\n } catch (GuzzleException $e) {\n
Log::error('Facebook API request error: ' . $e->getMessage());\n $this-
>handleApiError($e, $user);\n throw new FacebookApiException('Error
fetching owned ad accounts: ' . $e->getMessage());\n } catch (Exception $e)
{\n if (!$e instanceof FacebookApiException) {\n
Log::error('Error fetching owned Facebook ad accounts:
' . $e->getMessage());\n throw new FacebookApiException('Failed to
retrieve owned ad accounts: ' . $e->getMessage());\n }\n
throw $e;\n }\n }\n\n /**\n * Get aggregated ad creatives from
database grouped by creative (ad name) across all dates\n *\n * @param
Collection $adData Collection of FacebookAdData models\n * @param float|null
$roasTarget Optional ROAS target for win rate calculation\n * @param float|null
$spendTarget Optional spend target for win rate calculation\n * @return array
Aggregated creatives and optimization stats\n */\n public function
getAggregatedAdCreatives(Collection $adData, ?float $roasTarget = null, ?float
$spendTarget = null): array\n {\n // Group by creative only (ignoring
dates)\n $creativeGroups = $adData->groupBy('name');\n
$aggregatedCreatives = [];\n $winningCreativesCount = 0;\n
$activeCreativesCount = 0;\n\n foreach ($creativeGroups as $creativeId =>
$creativeAds) {\n // Calculate date range for this creative\n
$startDate = $creativeAds->min('data_date');\n $endDate = $creativeAds-
>max('data_date');\n $dateRange = $startDate;\n if
($startDate !== $endDate) {\n $dateRange .= ' to ' . $endDate;\n
}\n\n // Process creative group and append date range\n
$creative = $this->processCreativeAdsGroup($creativeId, $creativeAds);\n
$creative['date_range'] = $dateRange;\n $creative['is_winning'] =
false;\n\n // Only include active ads in the win rate calculation\n
if ($creative['status'] === FacebookAdData::STATUS_ACTIVE && $creative['metrics']
['spend'] > 0) {\n $activeCreativesCount++;\n\n //
Check if this creative meets the optimization targets\n if
($roasTarget > 0 && $spendTarget > 0) {\n $meetsRoasTarget =
$creative['metrics']['roas'] >= $roasTarget;\n $meetsSpendTarget
= $creative['metrics']['spend'] >= $spendTarget;\n
$creative['is_winning'] = $meetsRoasTarget && $meetsSpendTarget;\n\n
if ($creative['is_winning']) {\n $winningCreativesCount++;\n
}\n }\n }\n\n $aggregatedCreatives[] =
$creative;\n }\n\n // Sort creatives by spend (highest first)\n
usort($aggregatedCreatives, function ($a, $b) {\n return $b['metrics']
['spend'] <=> $a['metrics']['spend'];\n });\n\n // Calculate overall
win rate using only active creatives\n $winRate = $activeCreativesCount >
0 ? round(($winningCreativesCount / $activeCreativesCount) * 100, 2) : 0;\n\n
return [\n 'creatives' => $aggregatedCreatives,\n
'optimization' => [\n 'win_rate' => $winRate,\n
'winning_creatives_count' => $winningCreativesCount,\n
'active_creatives_count' => $activeCreativesCount,\n
'total_creatives' => count($aggregatedCreatives)\n ]\n ];\n }\
n\n /**\n * Get aggregated ad creatives from database grouped by creative\n
*\n * @param Collection $adData Collection of FacebookAdData models\n *
@return array Aggregated creatives by date\n */\n public function
getAggregatedAdCreativesPerDate(Collection $adData): array\n {\n // Group
by date and creative\n $aggregatedByDate = [];\n $adData-
>groupBy('data_date')->each(function ($dateGroup, $date) use (&$aggregatedByDate)
{\n // Format date to Y-m-d without time component\n
$formattedDate = date('Y-m-d', strtotime($date));\n\n // Group this
date's ads by creative\n $creativeGroups = $dateGroup->groupBy('name');\
n\n $aggregatedCreatives = [];\n\n foreach ($creativeGroups
as $creativeId => $creativeAds) {\n // Process creative group\n
$creative = $this->processCreativeAdsGroup($creativeId, $creativeAds);\n
$aggregatedCreatives[] = $creative;\n }\n\n // Sort creatives
by spend (highest first)\n usort($aggregatedCreatives, function ($a, $b)
{\n return $b['metrics']['spend'] <=> $a['metrics']['spend'];\n
});\n\n $aggregatedByDate[$formattedDate] = $aggregatedCreatives;\n
});\n\n // Sort dates in ascending order\n ksort($aggregatedByDate);\
n\n return $aggregatedByDate;\n }\n\n /**\n * Parse date range
into Facebook time_range format.\n * Accepts either predefined range
identifiers like 'today', 'last_7d', etc.,\n * or a JSON-encoded string with
'since' and 'until' dates in Y-m-d format,\n * or an array with 'since' and
'until' keys.\n *\n * @param array|string $dateRange Date range identifier
or array with exact dates\n * @return array\n */\n protected function
parseDateRange(array|string $dateRange): array\n {\n // If $dateRange is
already an array with since/until keys, use it directly\n if
(is_array($dateRange) && isset($dateRange['since']) && isset($dateRange['until']))
{\n return [\n 'since' => $dateRange['since'],\n
'until' => $dateRange['until'],\n ];\n }\n\n // Check if
$dateRange is a JSON string with since/until\n if (is_string($dateRange) &&
$this->isJsonString($dateRange)) {\n $decoded = json_decode($dateRange,
true);\n if (isset($decoded['since']) && isset($decoded['until'])) {\n
return [\n 'since' => $decoded['since'],\n
'until' => $decoded['until'],\n ];\n }\n }\n\n
// Handle predefined date range identifiers\n switch ($dateRange) {\n
case 'today':\n $since = Carbon::today()->format('Y-m-d');\n
$until = Carbon::today()->format('Y-m-d');\n break;\n
case 'yesterday':\n $since = Carbon::yesterday()->format('Y-m-d');\n
$until = Carbon::yesterday()->format('Y-m-d');\n break;\n
case 'last_7d':\n $since = Carbon::today()->subDays(7)->format('Y-m-
d');\n $until = Carbon::yesterday()->format('Y-m-d');\n
break;\n case 'last_30d':\n $since = Carbon::today()-
>subDays(30)->format('Y-m-d');\n $until = Carbon::yesterday()-
>format('Y-m-d');\n break;\n case 'this_month':\n
$since = Carbon::today()->startOfMonth()->format('Y-m-d');\n $until
= Carbon::today()->format('Y-m-d');\n break;\n case
'last_month':\n $since = Carbon::today()->subMonth()-
>startOfMonth()->format('Y-m-d');\n $until = Carbon::today()-
>subMonth()->endOfMonth()->format('Y-m-d');\n break;\n
default:\n // Default to last 30 days\n $since =
Carbon::today()->subDays(30)->format('Y-m-d');\n $until =
Carbon::yesterday()->format('Y-m-d');\n break;\n }\n\n
return [\n 'since' => $since,\n 'until' => $until,\
n ];\n }\n\n /**\n * Check if a string is valid JSON\n *\n
* @param string $string\n * @return bool\n */\n private function
isJsonString(string $string): bool\n {\n json_decode($string);\n
return json_last_error() === JSON_ERROR_NONE;\n }\n\n /**\n * Extract
numeric value from Facebook's video metric array format\n *\n * @param
array $metricArray The Facebook video metric array\n * @return float The
extracted numeric value\n */\n private function
extractVideoMetricValue(array $metricArray): float\n {\n if
(empty($metricArray)) {\n return 0;\n }\n\n foreach
($metricArray as $action) {\n if (isset($action['action_type']) &&
$action['action_type'] === 'video_view') {\n if
(isset($action['value'])) {\n if (is_array($action['value'])) {\
n // We'll store the full data in the video_metrics_data
JSON field\n return 0;\n } else {\n
return (float)($action['value']);\n }\n }\n
break;\n }\n }\n\n return 0;\n }\n\n /**\n *
Process a group of creative ads to calculate metrics and build creative object\n
*\n * @param int|string $creativeId ID of the creative\n * @param
Collection $creativeAds Collection of ads with the same creative ID\n * @return
array Processed creative with aggregated metrics\n */\n private function
processCreativeAdsGroup(int|string $creativeId, Collection $creativeAds): array\n
{\n // Use the latest ad for basic details\n $latestAd =
$creativeAds->sortByDesc('updated_at')->first();\n\n // Aggregate metrics\n
$totalImpressions = $creativeAds->sum('impressions');\n $totalReach =
$creativeAds->sum('reach');\n $totalClicks = $creativeAds->sum('clicks');\n
$totalSpend = $creativeAds->sum('spend');\n $totalRevenue = $creativeAds-
>sum('revenue');\n $totalPurchases = $creativeAds->sum('purchases');\n\n
// Aggregate video metrics\n $total3SecViews = $creativeAds-
>sum('video_3sec_views');\n $totalP75Views = $creativeAds-
>sum('video_p75_views');\n\n // Calculate derived metrics based on
aggregated values (not averaging pre-calculated values)\n
$ctr = $totalImpressions > 0 ? ($totalClicks / $totalImpressions) * 100 :
0;\n $cpm = $totalImpressions > 0 ? ($totalSpend / ($totalImpressions /
1000)) : 0;\n $cpp = $totalReach > 0 ? ($totalSpend / ($totalReach /
1000)) : 0;\n\n // Calculate ROAS, profit/loss\n $roas = $totalSpend
> 0 ? $totalRevenue / $totalSpend : 0;\n $profitLoss = $totalRevenue -
$totalSpend;\n\n // Calculate CPA if there are purchases\n $cpa =
($totalPurchases > 0 && $totalSpend > 0) ? $totalSpend / $totalPurchases : 0;\n\n
// Calculate conversion rate (as a percentage)\n $conversionRate =
$totalImpressions > 0 ? ($totalPurchases / $totalImpressions) * 100 : 0;\n\n
// Calculate video metrics\n $hookRate = $totalImpressions > 0 ?
($total3SecViews / $totalImpressions) * 100 : 0;\n $holdRate =
$total3SecViews > 0 ? ($totalP75Views / $total3SecViews) * 100 : 0;\n
$costPer3SecView = $total3SecViews > 0 ? $totalSpend / $total3SecViews : 0;\n\n
// Cap rates to reasonable limits (same as in getAdInsights)\n if ($hookRate
> 100) $hookRate = 100; // Hook rate can't exceed 100%\n if ($holdRate >
100) $holdRate = 100; // Hold rate can't exceed 100%\n if ($costPer3SecView
< 0) $costPer3SecView = 0; // Cost can't be negative\n\n // Build the
creative object with both static and aggregated data\n return [\n
'id' => $creativeId,\n 'status' => $latestAd->status ??
FacebookAdData::STATUS_ACTIVE,\n 'name' => $latestAd->name ?? '',\n
'adset' => $latestAd->adset ?? '',\n 'adset_id' => $latestAd-
>adset_id ?? '',\n 'campaign' => $latestAd->campaign ?? '',\n
'campaign_id' => $latestAd->campaign_id ?? '',\n 'thumbnail' =>
$latestAd->thumbnail ?? '',\n 'original_image_url' => $latestAd-
>original_image_url ?? '',\n 'message' => $latestAd->message ?? '',\n
'call_to_action' => $latestAd->call_to_action ?? '',\n
'call_to_action_value' => $latestAd->call_to_action_value ?? '',\n
'effective_object_story_id' => $latestAd->effective_object_story_id ?? '',\n
'metrics' => [\n 'impressions' => $totalImpressions,\n
'reach' => $totalReach,\n 'clicks' => $totalClicks,\n
'spend' => round($totalSpend, 2),\n 'ctr' => round($ctr, 2),\n
'cpm' => round($cpm, 2),\n 'cpp' => round($cpp, 2),\n
'roas' => round($roas, 2),\n 'cpa' => round($cpa, 2),\n
'conversion_rate' => round($conversionRate, 2),\n 'revenue' =>
round($totalRevenue, 2),\n 'profit_loss' => round($profitLoss, 2),\n
'purchases' => $totalPurchases,\n 'video_3sec_views' =>
$total3SecViews,\n 'video_p75_views' => $totalP75Views,\n
'hook_rate' => round($hookRate, 2),\n 'hold_rate' =>
round($holdRate, 2),\n 'cost_per_3sec_view' =>
round($costPer3SecView, 2),\n ]\n ];\n }\n\n /**\n *
@param float|int $hookRate\n * @param float|int $holdRate\n * @param float|
int $conversionRate\n * @return bool\n */\n private function
isConversionRateWarningFlag(float|int $hookRate, float|int $holdRate, float|int
$conversionRate): bool\n {\n $conversionRateWarningFlag = false;\n
if ($hookRate > 0 && $holdRate > 0 && $conversionRate > 0) {\n
$expectedConversionRate = (($hookRate / 100) * ($holdRate / 100)) * 5;\n
// Flag if conversion rate is 2x higher than expected\n if
($conversionRate > ($expectedConversionRate * 2)) {\n
$conversionRateWarningFlag = true;\n }\n }\n\n return
$conversionRateWarningFlag;\n }\n\n /**\n * Calculate weighted average
for a metric across multiple ads\n * This ensures metrics are properly weighted
by impressions or another factor\n *\n * @param Collection $items
Collection of ad data\n * @return float The weighted average of the metric\n
*/\n private function calculateWeightedAverage(Collection $items): float\n {\
n $totalWeightedValue = 0;\n $totalWeight = 0;\n\n foreach
($items as $item) {\n // Use video_3sec_views as weight if impressions
is zero (for video-only views like Story ads)\n $weight =
($item['impressions'] ?? 0) > 0 ? ($item['impressions'] ?? 0) :
($item['video_3sec_views'] ?? 0);\n\n if ($weight > 0) {\n
$totalWeightedValue += ($item['video_avg_time_watched'] ?? 0) * $weight;\n
$totalWeight += $weight;\n }\n }\n\n return $totalWeight >
0 ? $totalWeightedValue / $totalWeight : 0;\n }\n\n /**\n * Store
Facebook ad data in the aggregated table\n *\n * @param UserFacebookAccount
$facebookAccount The user's Facebook account\n * @param string $adAccountId The
ad account ID\n * @param string $startDate Start date in Y-m-d format\n *
@param string $endDate End date in Y-m-d format\n * @param array $apiData The
ad data from the API\n * @return array The stored aggregated ads data\n */\
n public function storeAdsDataInAggregatedTable(UserFacebookAccount
$facebookAccount, string $adAccountId, string $startDate, string $endDate, array
$apiData): array\n {\n $storedAds = [];\n\n foreach ($apiData as
$adData) {\n // Skip if creative_id is missing\n if
(empty($adData['creative_id'])) {\n continue;\n }\n\n
// Prepare ad data record\n $adRecord = [\n
'user_facebook_account_id' => $facebookAccount->id,\n
'ad_account_id' => $adAccountId,\n 'start_date' => $startDate,\n
'end_date' => $endDate,\n 'facebook_ad_id' => $adData['id'] ??
null,\n 'name' => $adData['name'] ?? null,\n 'status'
=> $adData['status'] ?? 'UNKNOWN',\n 'adset' => $adData['adset'] ??
null,\n 'adset_id' => $adData['adset_id'] ?? null,\n
'campaign' => $adData['campaign'] ?? null,\n 'campaign_id' =>
$adData['campaign_id'] ?? null,\n 'creative_id' =>
$adData['creative_id'],\n 'video_id' => $adData['video_id'] ??
null,\n 'thumbnail' => $adData['thumbnail'] ?? null,\n
'original_image_url' => $adData['original_image_url'] ?? null,\n
'message' => $adData['message'] ?? null,\n 'call_to_action' =>
$adData['call_to_action'] ?? null,\n 'call_to_action_value' =>
$adData['call_to_action_value'] ?? null,\n
'effective_object_story_id' => $adData['effective_object_story_id'] ?? null,\n
];\n\n // Add metrics if available\n if
(isset($adData['metrics']) && is_array($adData['metrics'])) {\n
$metrics = $adData['metrics'];\n $adRecord = array_merge($adRecord,
[\n 'impressions' => $metrics['impressions'] ?? 0,\n
'spend' => $metrics['spend'] ?? 0,\n 'clicks' =>
$metrics['clicks'] ?? 0,\n 'cpc' => $metrics['cpc'] ?? 0,\n
'cpm' => $metrics['cpm'] ?? 0,\n 'ctr' => $metrics['ctr'] ?? 0,\
n 'frequency' => $metrics['frequency'] ?? 0,\n
'reach' => $metrics['reach'] ?? 0,\n 'inline_link_clicks' =>
$metrics['inline_link_clicks'] ?? 0,\n
'cost_per_inline_link_click' => $metrics['cost_per_inline_link_click'] ?? 0,\n
'roas' => $metrics['roas'] ?? 0,\n 'cpa' => $metrics['cpa'] ??
0,\n 'conversion_rate' => $metrics['conversion_rate'] ?? 0,\n
'revenue' => $metrics['revenue'] ?? 0,\n 'profit_loss' =>
$metrics['profit_loss'] ?? 0,\n 'purchases' =>
$metrics['purchases'] ?? 0,\n\n // Include video metrics\n
'video_3sec_views' => $metrics['video_3sec_views'] ?? 0,\n
'video_p75_views' => $metrics['video_p75_views'] ?? 0,\n
'hook_rate' => $metrics['hook_rate'] ?? 0,\n 'hold_rate' =>
$metrics['hold_rate'] ?? 0,\n 'cost_per_3sec_view' =>
$metrics['cost_per_3sec_view'] ?? 0,\n
'conversion_rate_warning_flag' => $metrics['conversion_rate_warning_flag'] ??
false,\n 'video_metrics_data' =>
$metrics['video_metrics_data'] ?? null,\n ]);\n }\n\n
// Encode JSON fields\n if (isset($adData['actions'])) {\n
$adRecord['actions'] = json_encode($adData['actions']);\n }\n\n
if (isset($adData['action_values'])) {\n $adRecord['action_values']
= json_encode($adData['action_values']);\n }\n\n if
(isset($adData['cost_per_action_type'])) {\n
$adRecord['cost_per_action_type'] = json_encode($adData['cost_per_action_type']);\n
}\n\n // Create or update the record in a single query\n
$storedAd = FacebookAdDataAggregated::updateOrCreate(\n [\n
'user_facebook_account_id' => $facebookAccount->id,\n
'ad_account_id' => $adAccountId,\n 'start_date' => $startDate,\n
'end_date' => $endDate,\n 'facebook_ad_id' => $adData['id'] ??
null,\n ],\n $adRecord\n );\n\n
$storedAds[]
= $storedAd;\n }\n\n return $storedAds;\n }\n}\n",
"prFileContentAfter" : "<?php\n\nnamespace App\\Services;\n\nuse
Illuminate\\Support\\Facades\\DB;\nuse Illuminate\\Support\\Arr;\nuse App\\Models\\
FacebookAdData;\nuse Illuminate\\Support\\Facades\\Log;\nuse App\\Models\\
FacebookAdDataAggregated;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse
App\\Models\\UserFacebookAccount;\nuse App\\Exceptions\\FacebookApiException;\nuse
Exception;\nuse FacebookAds\\Api;\nuse FacebookAds\\Logger\\CurlLogger;\nuse
GuzzleHttp\\Client;\nuse Illuminate\\Support\\Carbon;\nuse GuzzleHttp\\Exception\\
GuzzleException;\nuse Illuminate\\Database\\Eloquent\\Collection;\nuse Psr\\
Container\\ContainerExceptionInterface;\nuse Psr\\Container\\
NotFoundExceptionInterface;\n\nclass FacebookAdService\n{\n protected string
$appId;\n\n protected string $appSecret;\n\n protected string $apiVersion;\n\
n protected string $redirectUri;\n\n /**\n * FacebookAdService
constructor.\n * @throws FacebookApiException\n */\n public function
__construct()\n {\n $this->appId =
config('services.facebook.business_client_id');\n $this->appSecret =
config('services.facebook.business_client_secret');\n $this->apiVersion =
config('services.facebook.api_version');\n $this->redirectUri =
config('services.facebook.business_redirect');\n\n if (empty($this->appId)
|| empty($this->appSecret)) {\n Log::error('Facebook app credentials not
set in configuration');\n throw new FacebookApiException('Facebook app
credentials are not properly configured');\n }\n }\n\n /**\n * Get
the login URL for Facebook OAuth.\n *\n * @return string\n * @throws
FacebookApiException\n */\n public function getLoginUrl(): string\n {\n
try {\n // Define the permissions we need\n $permissions = [\
n 'email',\n 'public_profile',\n
'ads_management',\n 'ads_read',\n
'business_management',\n 'pages_read_engagement',\n
'pages_show_list',\n 'pages_show_list',\n ];\n\n
// Generate state parameter for CSRF protection\n $state =
bin2hex(random_bytes(16));\n session(['fb_state' => $state]);\n\n
// Build the login URL manually\n $params = [\n
'client_id' => $this->appId,\n 'redirect_uri' => $this-
>redirectUri,\n 'state' => $state,\n 'response_type'
=> 'code',\n 'scope' => implode(',', $permissions),\n ];\
n\n return 'https://www.facebook.com/' . $this->apiVersion .
'/dialog/oauth?' . http_build_query($params);\n } catch (Exception $e) {\n
Log::error('Facebook login URL error: ' . $e->getMessage());\n throw new
FacebookApiException('Failed to generate Facebook login URL: ' . $e-
>getMessage());\n }\n }\n\n /**\n * Handle the callback from
Facebook OAuth.\n *\n * @param string $code The authorization code from
Facebook\n * @return array Access token details\n * @throws
FacebookApiException\n */\n public function handleCallback(string $code):
array\n {\n try {\n // Verify state parameter to prevent CSRF
attacks\n $state = request()->get('state');\n $sessionState =
session('fb_state');\n\n if (!$state || !$sessionState || $state !==
$sessionState) {\n throw new FacebookApiException('Invalid state
parameter, possible CSRF attack');\n }\n\n // Clear the state
from session\n session()->forget('fb_state');\n\n // Exchange
the authorization code for an access token\n $client = new Client();\n
$response = $client->post('https://graph.facebook.com/' . $this->apiVersion .
'/oauth/access_token', [\n 'form_params' => [\n
'client_id' => $this->appId,\n 'client_secret' => $this-
>appSecret,\n 'redirect_uri' => $this->redirectUri,\n
'code' => $code,\n ],\n ]);\n\n $data =
json_decode((string)$response->getBody(), true);\n\n if (!
isset($data['access_token'])) {\n throw new
FacebookApiException('Failed to obtain access token.');\n }\n\n
// Get the short-lived access token\n $accessToken =
$data['access_token'];\n\n // Exchange for a long-lived token\n
$response = $client->get('https://graph.facebook.com/' . $this->apiVersion .
'/oauth/access_token', [\n 'query' => [\n
'grant_type' => 'fb_exchange_token',\n 'client_id' => $this-
>appId,\n 'client_secret' => $this->appSecret,\n
'fb_exchange_token' => $accessToken,\n ],\n ]);\n\n
$data = json_decode((string)$response->getBody(), true);\n\n if (!
isset($data['access_token'])) {\n throw new
FacebookApiException('Failed to obtain long-lived access token.');\n }\
n\n $longLivedToken = $data['access_token'];\n $expiresIn =
$data['expires_in'] ?? 5184000; // Default to 60 days if not provided\n\n
// Get token debug info\n $response =
$client->get('https://graph.facebook.com/' . $this->apiVersion . '/debug_token', [\
n 'query' => [\n 'input_token' =>
$longLivedToken,\n 'access_token' => $this->appId . '|' . $this-
>appSecret,\n ],\n ]);\n\n $debugData =
json_decode((string)$response->getBody(), true);\n\n if (!
isset($debugData['data']['user_id'])) {\n throw new
FacebookApiException('Failed to get user ID from token.');\n }\n\n
// Calculate token expiration date\n $expiresAt = Carbon::now()-
>addSeconds($expiresIn);\n\n // Return token details and ad account ID\n
return [\n 'access_token' => $longLivedToken,\n
'expires_at' => $expiresAt,\n 'user_id' => $debugData['data']
['user_id'],\n ];\n } catch (GuzzleException|
NotFoundExceptionInterface|ContainerExceptionInterface $e) {\n
Log::error('Facebook API request error: ' . $e->getMessage());\n throw
new FacebookApiException('Error making request to Facebook: ' . $e->getMessage());\
n } catch (Exception $e) {\n if (!$e instanceof
FacebookApiException) {\n Log::error('Facebook token exchange error:
' . $e->getMessage());\n throw new FacebookApiException('Error
exchanging Facebook code for token: ' . $e->getMessage());\n }\n
throw $e;\n }\n }\n\n /**\n * Save the Facebook access token for a
user.\n *\n * @param Authenticatable $user\n * @param array
$tokenDetails\n * @return UserFacebookAccount\n */\n public function
saveUserToken(Authenticatable $user, array $tokenDetails): UserFacebookAccount\n
{\n $currentWorkspaceId = $user->current_team_id;\n\n $userData = [\n
'team_id' => $currentWorkspaceId,\n 'access_token' =>
$tokenDetails['access_token'],\n 'token_expires_at' =>
$tokenDetails['expires_at'],\n 'account_status' =>
UserFacebookAccount::STATUS_ACTIVE,\n ];\n\n // Also save the
ad_account_id if it's included in the token details\n if
(isset($tokenDetails['ad_account_id'])) {\n $userData['ad_account_id'] =
$tokenDetails['ad_account_id'];\n }\n\n return
UserFacebookAccount::updateOrCreate(\n ['user_id' =>
WorkspaceService::getWorkspaceUser()?->id, 'team_id' => $currentWorkspaceId],\n
$userData\n );\n }\n\n /**\n * Get the user's ad accounts from
Facebook.\n *\n * @param Authenticatable $user\n * @param string|null
$businessId Optional business ID to use a specific account\n * @return array\n
* @throws FacebookApiException\n */\n public function
getAdAccounts(Authenticatable $user, ?string $businessId = null): array\n {\n
try {\n $account = $this->getUserFacebookAccount($user, $businessId);\n\
n // Initialize the Facebook Ads API with the user's access token\n
$api = Api::init(null, null, $account->access_token);\n\n // Enable
debugging if needed\n if (config('app.debug')) {\n $api-
>setLogger(new CurlLogger());\n }\n\n // Get user's ad
accounts using the /me/adaccounts endpoint\n $client = new Client();\n
$response = $client->get('https://graph.facebook.com/' . $this->apiVersion .
'/me/adaccounts', [\n 'query' => [\n
'access_token' => $account->access_token,\n 'fields' =>
'id,name,account_id,account_status,amount_spent,business_name,currency,timezone_nam
e',\n ],\n ]);\n\n $data =
json_decode((string)$response->getBody(), true);\n\n if (!
isset($data['data'])) {\n throw new FacebookApiException('Failed to
fetch ad accounts data');\n }\n\n // Update last fetch time\n
$account->last_data_fetch = now();\n $account->save();\n\n
return $data['data'];\n } catch (GuzzleException $e) {\n
Log::error('Facebook API request error: ' . $e->getMessage());\n $this-
>handleApiError($e, $user);\n throw new FacebookApiException('Error
connecting to Facebook API: ' . $e->getMessage());\n } catch (Exception
$e) {\n if (!$e instanceof FacebookApiException) {\n
Log::error('Facebook ad accounts error: ' . $e->getMessage());\n
throw new FacebookApiException('Error fetching Facebook ad accounts: ' . $e-
>getMessage());\n }\n throw $e;\n }\n }\n\n /**\
n * Get campaigns for a specific ad account.\n *\n * @param
Authenticatable $user\n * @param string $adAccountId\n * @param string|null
$businessId Optional business ID to use a specific account\n * @return array\n
* @throws FacebookApiException\n */\n public function
getCampaigns(Authenticatable $user, string $adAccountId, ?string $businessId =
null): array\n {\n try {\n $account = $this-
>getUserFacebookAccount($user, $businessId, $adAccountId);\n\n //
Initialize the Facebook Ads API with the user's access token\n $api =
Api::init(null, null, $account->access_token);\n\n // Enable debugging
if needed\n if (config('app.debug')) {\n $api-
>setLogger(new CurlLogger());\n }\n\n // Get campaigns for
the specified ad account\n $client = new Client();\n
$response = $client->get('https://graph.facebook.com/' . $this->apiVersion . '/' .
$account->ad_account_id . '/campaigns', [\n 'query' => [\n
'access_token' => $account->access_token,\n 'fields' =>
'id,name,objective,status,spend_cap,budget_remaining,daily_budget,lifetime_budget,s
tart_time,stop_time,updated_time,created_time',\n 'limit' =>
100,\n ],\n ]);\n\n $data =
json_decode((string)$response->getBody(), true);\n\n if (!
isset($data['data'])) {\n throw new FacebookApiException('Failed to
fetch campaign data');\n }\n\n return $data['data'];\n
} catch (GuzzleException $e) {\n Log::error('Facebook API request error:
' . $e->getMessage());\n $this->handleApiError($e, $user);\n
throw new FacebookApiException('Error connecting to Facebook API: ' . $e-
>getMessage());\n } catch (Exception $e) {\n if (!$e instanceof
FacebookApiException) {\n Log::error('Facebook campaigns error: ' .
$e->getMessage());\n throw new FacebookApiException('Error fetching
Facebook campaigns: ' . $e->getMessage());\n }\n throw $e;\n
}\n }\n\n /**\n * Disconnect a Facebook business account for a user.\n
*\n * @param Authenticatable $user\n * @param string|null $businessId
Optional business ID to disconnect a specific account\n * @return bool\n
*/\n public function disconnectAccount(Authenticatable $user, ?string
$businessId = null): bool\n {\n $currentWorkspaceId = $user-
>current_team_id;\n\n $query = UserFacebookAccount::where('team_id',
$currentWorkspaceId)\n ->where('user_id', $user->id)\n -
>where('account_status', UserFacebookAccount::STATUS_ACTIVE);\n\n // If a
specific business ID is provided, disconnect only that account\n if
($businessId) {\n $query->where('business_id', $businessId);\n }\
n\n $accounts = $query->get();\n\n if ($accounts->isNotEmpty()) {\n
foreach ($accounts as $account) {\n $account->account_status =
UserFacebookAccount::STATUS_DISCONNECTED;\n $account->save();\n
}\n return true;\n }\n\n return false;\n }\n\n /**\n
* Check if the user has a valid Facebook connection.\n *\n * @return bool\n
*/\n public function isConnected(): bool\n {\n $user =
WorkspaceService::getWorkspaceUser();\n $currentWorkspaceId = $user-
>current_team_id;\n\n return UserFacebookAccount::where('user_id', $user?-
>id)\n ->where('team_id', $currentWorkspaceId)\n -
>where('account_status', UserFacebookAccount::STATUS_ACTIVE)\n -
>whereNotNull([\n 'access_token',\n 'business_id',\n
'ad_account_id',\n 'optimization',\n 'roas_target',\n
'spend_target',\n ])\n ->exists();\n }\n\n /**\n *
Get the user's business accounts from Facebook.\n *\n * @param
Authenticatable $user\n * @param string|null $businessId Optional business ID
to use a specific account\n * @return array\n * @throws
FacebookApiException|GuzzleException\n */\n public function
getBusinessAccounts(Authenticatable $user, ?string $businessId = null): array\n
{\n try {\n $account = $this->getUserFacebookAccount($user,
$businessId);\n\n // Make a GET request to fetch business accounts with
profile pictures\n $client = new Client();\n $response =
$client->get('https://graph.facebook.com/' . $this->apiVersion . '/me/businesses',
[\n 'query' => [\n 'access_token' => $account-
>access_token,\n 'fields' =>
'id,name,verification_status,picture{url,width,height}',\n ],\n
]);\n\n $data = json_decode((string)$response->getBody(), true);\n\n
return $data['data'] ?? [];\n } catch (Exception $e) {\n
Log::error('Error fetching Facebook business accounts: ' . $e->getMessage());\n
throw new FacebookApiException('Failed to retrieve business accounts: ' . $e-
>getMessage());\n }\n }\n\n /**\n * Update or create a Facebook
business account for a user.\n * This method supports the multi-business
account feature by creating separate\n * account records for each business ID.\
n *\n * @param Authenticatable $user\n * @param string $businessId The
business ID to connect\n * @param string|null $businessName Optional business
name\n * @return UserFacebookAccount\n * @throws FacebookApiException\n
*/\n public function updateBusinessId(Authenticatable $user, string $businessId,
?string $businessName = null): UserFacebookAccount\n {\n try {\n
$currentWorkspaceId = $user->current_team_id;\n\n // First check if we
already have an account with this specific business_id\n $account =
UserFacebookAccount::where([\n 'team_id' => $currentWorkspaceId,\n
'user_id' => $user->id,\n 'business_id' => $businessId,\n
'ad_account_id' => null\n ])->first();\n\n if (!$account) {\n
// No existing account with this business_id, create a new one\n
$account = new UserFacebookAccount([\n 'team_id' =>
$currentWorkspaceId,\n 'user_id' => $user->id,\n
'business_id' => $businessId,\n 'business_name' =>
$businessName,\n ]);\n\n // Get token information
from another active account if available\n $existingAccount =
UserFacebookAccount::where('team_id', $currentWorkspaceId)\n -
>where('account_status', UserFacebookAccount::STATUS_ACTIVE)\n -
>first();\n\n if ($existingAccount) {\n $account-
>access_token = $existingAccount->access_token;\n $account-
>token_expires_at = $existingAccount->token_expires_at;\n
$account->refresh_token = $existingAccount->refresh_token;\n }\n
} else {\n // We found an existing account with this business_id,
update it\n if ($businessName) {\n $account-
>business_name = $businessName;\n }\n }\n\n //
Make sure the account is active\n $account->account_status =
UserFacebookAccount::STATUS_ACTIVE;\n $account->save();\n\n
return $account;\n } catch (Exception $e) {\n Log::error('Error
updating Facebook business ID: ' . $e->getMessage());\n throw new
FacebookApiException('Failed to update business ID: ' . $e->getMessage());\n
}\n }\n\n /**\n * Update the ad account ID for a specific Facebook
business account.\n *\n * @param Authenticatable $user\n * @param
string $adAccountId\n * @param string|null $businessId Optional specific
business ID to update\n * @param string|null $adAccountName\n * @return
UserFacebookAccount\n * @throws FacebookApiException\n */\n public
function updateAdAccountId(Authenticatable $user, string $adAccountId, ?string
$businessId = null, ?string $adAccountName = null): UserFacebookAccount\n {\n
try {\n $account = $this->getUserFacebookAccount($user, $businessId);\n\
n // Format the ad account ID if needed\n if (!
str_starts_with($adAccountId, 'act_')) {\n $adAccountId = 'act_' .
$adAccountId;\n }\n\n // Update the account with the ad
account ID\n $account->ad_account_id = $adAccountId;\n
$account->ad_account_name = $adAccountName;\n $account->save();\n\n
return $account;\n } catch (Exception $e) {\n Log::error('Error
updating Facebook ad account ID: ' . $e->getMessage());\n throw new
FacebookApiException('Failed to update ad account ID: ' . $e->getMessage());\n
}\n }\n\n /**\n * Get comprehensive ad insights with performance
metrics.\n *\n * @param Authenticatable $user\n * @param string
$adAccountId\n * @param array|string $dateRange\n * @param array $metrics\n
* @param string|null $businessId Optional
business ID to use a specific account\n * @return array\n * @throws
FacebookApiException\n * @throws GuzzleException\n */\n public function
getAdInsights(Authenticatable $user, string $adAccountId, array|string $dateRange,
array $metrics = [], ?string $businessId = null): array\n {\n try {\n
$account = $this->getUserFacebookAccount($user, $businessId, $adAccountId);\n\n
// Default metrics if not specified\n if (empty($metrics)) {\n
$metrics = [\n 'impressions',\n 'reach',\n
'clicks',\n 'spend',\n 'ctr',\n
'cpm',\n 'cpp',\n 'purchase_roas',\n
'cost_per_inline_link_click',\n 'cost_per_action_type',\n
'actions',\n 'action_values',\n // Video
metrics for Hook Rate and Hold Rate\n
'video_p25_watched_actions',\n 'video_p50_watched_actions',\n
'video_p75_watched_actions',\n
'video_avg_time_watched_actions',\n 'video_play_actions',\n
//'video_play_curve_actions',\n ];\n }\n\n
$creativeFields = implode(',', [\n 'id',\n 'name',\n
'thumbnail_url',\n 'image_url',\n //'video_id',\n
//'effective_object_story_id',\
n //'object_story_spec{link_data{message,call_to_action{type,value}}
}'\n ]);\n\n $timeRange = $this->parseDateRange($dateRange);\
n\n // Get ads with their creatives and insights\n $client =
new Client();\n $response =
$client->get(\"https://graph.facebook.com/$this->apiVersion/$account-
>ad_account_id/ads\", [\n 'query' => [\n
'access_token' => $account->access_token,\n 'fields' =>
'id,name,status,adset{id,name,campaign{id,name}},creative{' . $creativeFields .
'},insights.time_range(' . json_encode($timeRange) . '){' . implode(',',
$metrics) . '}',\n 'limit' => 250\n ],\n
]);\n\n $data = json_decode((string)$response->getBody(), true);\n\n
if (!Arr::exists($data, 'data') || empty($data['data'])) {\n return
[];\n }\n\n $creativeIds = [];\n foreach
($data['data'] as $ad) {\n if ($cid = data_get($ad, 'creative.id'))
{\n $creativeIds[$cid] = true;\n }\
n }\n // Batch-fetch thumbnail_url at 600 px\n
foreach (array_chunk(array_keys($creativeIds), 50) as $chunk) {\n
$resp = $client->get(\"https://graph.facebook.com/$this->apiVersion/\", [\n
'query' => [\n 'ids' => implode(',', $chunk),\n
'fields' => 'thumbnail_url',\n 'thumbnail_width' => 600,\n
'thumbnail_height' => 600,\n 'access_token' => $account-
>access_token,\n ],\n ]);\n
$thumbs = json_decode($resp->getBody(), true);\n foreach
($data['data'] as &$ad) {\n $cid = $ad['creative']['id'] ??
null;\n if ($cid && isset($thumbs[$cid]['thumbnail_url'])) {\n
$ad['creative']['thumbnail_url_hd'] = $thumbs[$cid]['thumbnail_url'];\n
}\n }\n }\n\n /*// Create a map from ad ID to
video ID\n $adToVideoMap = collect($data['data'])->mapWithKeys(function
($item) {\n return [$item['id'] => data_get($item,
'creative.video_id')];\n })->filter()->unique()->toArray();\n
$allVideos = [];\n $videoChunks = array_chunk($adToVideoMap, 25, true);\
n // Fetch video details for all identified video IDs\n
foreach ($videoChunks as $chunk) {\n $response = $client-
>get(\"https://graph.facebook.com/$this->apiVersion/$adAccountId/advideos\", [\n
'query' => [\n 'video_ids' => implode(',', $chunk),\n
'fields' => 'id,source,length,thumbnails{uri,is_preferred,width,height}',\n
'access_token' => $account->access_token,\n ],\
n ]);\n $payload = json_decode((string)$response-
>getBody(), true);\n if (!empty($payload) &&
isset($payload['data'])) {\n $allVideos[] = $payload['data'];\n
}\n }\n return $allVideos;*/\n\n // Process and
structure the data\n $processedAds = [];\n foreach
($data['data'] as $ad) {\n $insights = $ad['insights']['data'][0] ??
[];\n\n // Get all ad details\n $adDetails = [\n
'id' => $ad['id'],\n 'name' => $ad['name'],\n
'status' => $ad['status'],\n 'adset' => $ad['adset']['name'] ??
'',\n 'adset_id' => $ad['adset']['id'] ?? '',\n
'campaign' => $ad['adset']['campaign']['name'] ?? '',\n
'campaign_id' => $ad['adset']['campaign']['id'] ?? '',\n
'thumbnail' => $ad['creative']['thumbnail_url'] ?? '',\n
'creative_id' => $ad['creative']['id'] ?? '',\n 'video_id' =>
$ad['creative']['video_id'] ?? '',\n 'message' =>
$ad['creative']['object_story_spec']['link_data']['message'] ?? $ad['creative']
['object_story_spec']['video_data']['message'] ?? '',\n
'call_to_action' => $ad['creative']['object_story_spec']['link_data']
['call_to_action']['type'] ?? $ad['creative']['object_story_spec']['video_data']
['call_to_action']['type'] ?? '',\n 'call_to_action_value' =>
$ad['creative']['object_story_spec']['link_data']['call_to_action']['value']
['link'] ?? $ad['creative']['object_story_spec']['video_data']['call_to_action']
['value']['link'] ?? '',\n 'effective_object_story_id' =>
$ad['creative']['effective_object_story_id'] ?? '',\n
'original_image_url' => $ad['creative']['thumbnail_url_hd'] ?? '',\n
];\n\n // Directly add metrics to the ad details with null-safe
access\n $adDetails['metrics'] = [\n
'impressions' => $insights['impressions'] ?? 0,\n 'reach' =>
$insights['reach'] ?? 0,\n 'clicks' => $insights['clicks'] ??
0,\n 'spend' => $insights['spend'] ?? 0,\n
'ctr' => $insights['ctr'] ?? 0,\n 'cpm' => $insights['cpm'] ??
0,\n 'cpp' => $insights['cpp'] ?? 0,\n
'cost_per_inline_link_click' => $insights['cost_per_inline_link_click'] ?? 0,\n
'cost_per_action_type' => $insights['cost_per_action_type'] ?? [],\n
'actions' => $insights['actions'] ?? [],\n 'action_values' =>
$insights['action_values'] ?? [],\n // Extract numeric values
directly from video metrics arrays with clearer names\n
'p25_video_views' => $this-
>extractVideoMetricValue($insights['video_p25_watched_actions'] ?? []),\n
'p50_video_views' => $this-
>extractVideoMetricValue($insights['video_p50_watched_actions'] ?? []),\n
'p75_video_views' => $this-
>extractVideoMetricValue($insights['video_p75_watched_actions'] ?? []),\n
'avg_watch_time' => $this-
>extractVideoMetricValue($insights['video_avg_time_watched_actions'] ?? []),\n
'play_actions' => $this->extractVideoMetricValue($insights['video_play_actions'] ??
[]),\n ];\n\n $spend = (float)($insights['spend'] ??
0);\n\n $cpa = 0;\n $revenue = 0;\n
$purchasesTotal = 0;\n $conversionRate = 0;\n
$totalConversions = 0;\n\n // Count total conversions from actions\n
if (!empty($insights['actions']) && is_array($insights['actions'])) {\n
// Meaningful conversion actions\n $conversionTypes = [\n
'omni_purchase',\n 'lead',\n
'complete_registration',\n 'subscribe',\n
'add_to_cart',\n 'initiate_checkout',\n
'add_payment_info',\n 'start_trial',\n
'add_to_wishlist',\n ];\n\n foreach
($insights['actions'] as $action) {\n if
(in_array($action['action_type'] ?? '', $conversionTypes)) {\n
$totalConversions += (int)($action['value'] ?? 0);\n }\n\n
// Extract purchase actions specifically for revenue\n if
(($action['action_type'] ?? '') === 'omni_purchase') {\n
$purchasesTotal += (float)($action['value'] ?? 0);\n }\n
}\n\n // Calculate CPA if there are conversions and spend\n
if ($totalConversions > 0 && $spend > 0) {\n $cpa = $spend /
$totalConversions;\n }\n\n // Calculate
conversion rate (as a percentage)\n $impressions
= (int)($insights['impressions'] ?? 0);\n if ($impressions > 0)
{\n $conversionRate = ($totalConversions / $impressions) *
100;\n }\n }\n\n
/*$purchaseActionTypes = [\n 'purchase',\n
'offsite_conversion.fb_pixel_purchase',\n
'onsite_web_purchase',\n 'onsite_web_app_purchase',\n
'omni_purchase',\n 'web_in_store_purchase',\n
'app_custom_event.fb_mobile_purchase'\n ];*/\n\n //
Calculate Revenue from action_values\n if (!
empty($insights['action_values']) && is_array($insights['action_values'])) {\n
foreach ($insights['action_values'] as $actionValue) {\n if
($actionValue['action_type'] === 'omni_purchase') {\n
$revenue = (float)($actionValue['value'] ?? 0);\n }\n
}\n }\n\n $roas = $spend > 0 ? $revenue / $spend :
0;\n $profitLoss = $revenue - $spend;\n\n // Add all
metrics to the response\n $adDetails['metrics']['roas'] =
round($roas, 2);\n $adDetails['metrics']['cpa'] = round($cpa, 2);\n
$adDetails['metrics']['conversion_rate'] = round($conversionRate, 2);\n
$adDetails['metrics']['revenue'] = round($revenue, 2);\n
$adDetails['metrics']['profit_loss'] = round($profitLoss, 2);\n
$adDetails['metrics']['purchases'] = $purchasesTotal;\n\n // Extract
all video metrics values from Facebook's available metrics\n
$videoP25Views = $this-
>extractVideoMetricValue($insights['video_p25_watched_actions'] ?? []);\n
$videoP50Views = $this-
>extractVideoMetricValue($insights['video_p50_watched_actions'] ?? []);\n
$videoP75Views = $this-
>extractVideoMetricValue($insights['video_p75_watched_actions'] ?? []);\n
$videoAvgTimeWatched = $this-
>extractVideoMetricValue($insights['video_avg_time_watched_actions'] ?? []);\n
$videoPlayActions = $this-
>extractVideoMetricValue($insights['video_play_actions'] ?? []);\n\n
// Extract 3-second video views from the actions array (video_view action type)\n
$video3SecViews = 0;\n if (!empty($insights['actions']) &&
is_array($insights['actions'])) {\n foreach
($insights['actions'] as $action) {\n if
(($action['action_type'] ?? '') === 'video_view') {\n
$video3SecViews = (int)($action['value'] ?? 0);\n
break;\n }\n }\n }\n\n
// Calculate Hook Rate: (3-second video views / total impressions) * 100\n
$hookRate = 0;\n if ($adDetails['metrics']['impressions'] > 0 &&
$video3SecViews > 0) {\n $hookRate = ($video3SecViews /
$adDetails['metrics']['impressions']) * 100;\n }\n\
n // Calculate Hold Rate: (P75 views / 3-second video views) * 100\n
// This is a more reliable indicator of viewers who watched a significant portion
of the video\n $holdRate = 0;\n if ($video3SecViews >
0 && $videoP75Views > 0) {\n $holdRate = ($videoP75Views /
$video3SecViews) * 100;\n }\n\n // Calculate Cost per
3-Second View: Total ad spend / Number of 3-second video views\n
$costPer3SecView = 0;\n if ($video3SecViews > 0 && $spend > 0) {\n
$costPer3SecView = $spend / $video3SecViews;\n }\n\
n // Handle edge cases - cap rates to reasonable limits\n
if ($hookRate > 100) $hookRate = 100; // Hook rate can't exceed 100%\n
if ($holdRate > 100) $holdRate = 100; // Hold rate can't exceed 100%\n
if ($costPer3SecView < 0) $costPer3SecView = 0; // Cost can't be negative\n\n
// Check for Conversion Rate Consistency\n
$conversionRateWarningFlag = $this->isConversionRateWarningFlag($hookRate,
$holdRate, $conversionRate);\n\n // Store all raw video metrics data
for future reference and analysis\n $videoMetricsData = [\n
// Raw metrics data from Facebook API\n 'raw_data' => [\n
'video_p25_watched_actions' => $insights['video_p25_watched_actions'] ?? [],\n
'video_p50_watched_actions' => $insights['video_p50_watched_actions'] ?? [],\n
'video_p75_watched_actions' => $insights['video_p75_watched_actions'] ?? [],\n
'video_avg_time_watched_actions' => $insights['video_avg_time_watched_actions'] ??
[],\n 'video_play_actions' =>
$insights['video_play_actions'] ?? [],\n
'video_play_curve_actions' => $insights['video_play_curve_actions'] ?? []\n
],\n // Clean extracted values with consistent naming\n
'metrics' => [\n // Basic video view metrics\n
'video_3sec_views' => $video3SecViews, // From video_view action type\n
'video_p25_views' => $videoP25Views,\n 'video_p50_views' =>
$videoP50Views,\n 'video_p75_views' => $videoP75Views,\n
'avg_watch_time' => $videoAvgTimeWatched,\n 'play_actions'
=> $videoPlayActions,\n\n // Calculated performance metrics\
n 'hook_rate' => round($hookRate, 2),\n
'hold_rate' => round($holdRate, 2),\n 'cost_per_3sec_view'
=> round($costPer3SecView, 2)\n ]\n ];\n\n
// Add all video metrics to the metrics array with consistent names\n
$adDetails['metrics']['video_3sec_views'] = $video3SecViews; // From video_view
action type\n $adDetails['metrics']['video_p25_views'] =
$videoP25Views;\n $adDetails['metrics']['video_p50_views'] =
$videoP50Views;\n $adDetails['metrics']['video_p75_views'] =
$videoP75Views;\n $adDetails['metrics']['video_avg_time_watched'] =
$videoAvgTimeWatched;\n $adDetails['metrics']['video_play_actions']
= $videoPlayActions;\n\n // Calculated performance metrics\n
$adDetails['metrics']['hook_rate'] = round($hookRate, 2);\n
$adDetails['metrics']['hold_rate'] = round($holdRate, 2);\n
$adDetails['metrics']['cost_per_3sec_view'] = round($costPer3SecView, 2);\n
$adDetails['metrics']['conversion_rate_warning_flag'] =
$conversionRateWarningFlag;\n $adDetails['metrics']
['video_metrics_data'] = $videoMetricsData;\n\n $processedAds[] =
$adDetails;\n }\n\n return $processedAds;\n } catch
(Exception $e) {\n Log::error('Error fetching Facebook ad insights: ' .
$e->getMessage());\n throw new FacebookApiException('Failed to retrieve
ad insights: ' . $e->getMessage());\n }\n }\n\n /**\n * Get a
user's Facebook account, optionally for a specific business or ad account.\n *\
n * @param Authenticatable $user\n * @param string|null $businessId
Optional business ID to retrieve a specific account\n * @param string|null
$adAccountId Optional ad account ID to retrieve a specific account\n * @return
UserFacebookAccount\n * @throws FacebookApiException\n */\n protected
function getUserFacebookAccount(Authenticatable $user, ?string $businessId =
null, ?string $adAccountId = null): UserFacebookAccount\n {\n
$currentWorkspaceId = $user->current_team_id;\n\n $query =
UserFacebookAccount::where('team_id', $currentWorkspaceId)\n -
>where('account_status', UserFacebookAccount::STATUS_ACTIVE);\n\n // If a
specific business ID is requested, use that\n if ($businessId) {\n
$query->where('business_id', $businessId);\n }\n\n // If a specific
ad account ID is requested, use that\n if ($adAccountId) {\n //
Format ad account ID if needed\n if (!str_starts_with($adAccountId,
'act_')) {\n $adAccountId = 'act_' . $adAccountId;\n }\n\
n $query->where('ad_account_id', $adAccountId);\n }\n\n
$account = $query->first();\n\n if (!$account) {\n $errorMessage
= $businessId\n ? \"No connected Facebook account found for business
ID: $businessId\"\n : ($adAccountId\n ? \"No
connected Facebook account found for ad account ID: $adAccountId\"\n
: 'No connected Facebook account found for this workspace');\n throw new
FacebookApiException($errorMessage);\n }\n\n return $account;\n }\
n\n /**\n * Handle Facebook API errors.\n *\n * @param Exception $e\
n * @param Authenticatable $user\n * @throws FacebookApiException\n */\
n protected function handleApiError(Exception $e, Authenticatable $user): void\n
{\n Log::error('Facebook API error: ' . $e->getMessage(), [\n
'user_id' => $user->id,\n ]);\n\n // Check if the error is due to
expired tokens\n $errorMessage = $e->getMessage();\n $errorCode = $e
instanceof GuzzleException ? 0 : $e->getCode();\n\n if ($errorCode == 190
||\n
stripos($errorMessage, 'expired') !== false ||\n
stripos($errorMessage, 'invalid access token') !== false) {\n // Mark
account as requiring reconnection\n $account =
UserFacebookAccount::where('team_id', $user->current_team_id)->first();\n
if ($account) {\n $account->account_status =
UserFacebookAccount::STATUS_EXPIRED;\n $account->save();\n
}\n }\n }\n\n /**\n * Get all Facebook accounts for a user's
team.\n *\n * @param Authenticatable $user\n * @return array\n */\n
public function getUserFacebookAccounts(Authenticatable $user): array\n {\n
try {\n $currentWorkspaceId = $user->current_team_id;\n\n
$accounts = UserFacebookAccount::where('team_id', $currentWorkspaceId)\n
->whereNotNull('business_id')\n ->whereNotNull('ad_account_id')\n
->where('account_status', UserFacebookAccount::STATUS_ACTIVE)\n -
>get()\n ->toArray();\n\n return [\n
'success' => true,\n 'accounts' => $accounts\n ];\n
} catch (Exception $e) {\n Log::error('Error fetching Facebook accounts:
' . $e->getMessage(), [\n 'user_id' => $user->id,\n ]);\
n\n return [\n 'success' => false,\n
'message' => 'Failed to retrieve Facebook accounts: ' . $e->getMessage()\n
];\n }\n }\n\n /**\n * Update multiple ad account IDs for a
specific business ID.\n *\n * @param Authenticatable $user\n * @param
array $adAccounts Array of ad account data with id and name\n * @param string
$businessId Optional specific business ID to update\n * @param string|null
$businessName Optional business name\n * @return array Updated accounts\n *
@throws FacebookApiException\n */\n public function
updateMultipleAdAccountIds(Authenticatable $user, array $adAccounts, string
$businessId, ?string $businessName = null): array\n {\n try {\n
$currentWorkspaceId = $user->current_team_id;\n $updatedAccounts = [];\
n\n // Begin transaction\n DB::beginTransaction();\n\n
foreach ($adAccounts as $adAccountData) {\n // Validate required
fields\n if (!isset($adAccountData['id'])) {\n
throw new FacebookApiException('Each ad account must have an ID');\n
}\n\n $adAccountId = $adAccountData['id'];\n
$adAccountName = $adAccountData['name'] ?? null;\n\n // Format the
ad account ID if needed\n if (!str_starts_with($adAccountId,
'act_')) {\n $adAccountId = 'act_' . $adAccountId;\n
}\n\n // Find existing account or create a new one\n
$account = UserFacebookAccount::firstOrNew([\n 'team_id' =>
$currentWorkspaceId,\n 'user_id' => $user->id,\n
'business_id' => $businessId,\n 'business_name' =>
$businessName,\n 'ad_account_id' => $adAccountId\n
]);\n\n // If it's a new account, set up necessary fields\n
if (!$account->exists) {\n // Try to get the access token from
an existing account\n $existingAccount =
UserFacebookAccount::where('team_id', $currentWorkspaceId)\n
->where('user_id', $user->id)\n ->where('account_status',
UserFacebookAccount::STATUS_ACTIVE)\n ->first();\n\n
// Copy relevant fields from existing account\n $account-
>access_token = $existingAccount?->access_token;\n $account-
>token_expires_at = $existingAccount?->token_expires_at;\n
$account->refresh_token = $existingAccount?->refresh_token;\n
$account->account_status = UserFacebookAccount::STATUS_ACTIVE;\n }\
n\n // Update the account with the ad account info\n
$account->ad_account_id = $adAccountId;\n if ($adAccountName) {\n
$account->ad_account_name = $adAccountName;\n }\n\n
$account->save();\n $updatedAccounts[] = $account;\n }\n\
n // Delete redundant records after multi create\n
UserFacebookAccount::where('team_id', $currentWorkspaceId)\n -
>whereNull('business_id')\n ->whereNull('business_name')\n
->where('ad_account_id', '')\n ->delete();\n\n // Commit
transaction\n DB::commit();\n\n return $updatedAccounts;\n
} catch (Exception $e) {\n // Roll back in case of error\n
DB::rollBack();\n Log::error('Error updating multiple Facebook ad
account IDs: ' . $e->getMessage());\n throw new
FacebookApiException('Failed to update multiple ad account IDs: ' . $e-
>getMessage());\n }\n }\n\n /**\n * Update optimization targets
for multiple Facebook ad accounts.\n *\n * @param Authenticatable $user\n
* @param array $adAccountIds Array of ad account IDs to update\n * @param
string $optimization Optimization strategy\n * @param float $roasTarget ROAS
target value\n * @param float $spendTarget Spend target value\n * @param
string|null $businessId Optional specific business ID\n * @return array Updated
accounts\n * @throws FacebookApiException\n */\n public function
updateMultipleOptimizationTargets(\n Authenticatable $user,\n array
$adAccountIds,\n string $optimization,\n float $roasTarget,\n
float $spendTarget,\n ?string $businessId = null\n ): array {\n
try {\n $currentWorkspaceId = $user->current_team_id;\n
$updatedAccounts = [];\n\n // Begin transaction\n
DB::beginTransaction();\n\n foreach ($adAccountIds as $adAccountId) {\n
// Format the ad account ID if needed\n if (!
str_starts_with($adAccountId, 'act_')) {\n $adAccountId = 'act_'
. $adAccountId;\n }\n\n // Find the account to
update\n $query = UserFacebookAccount::where('team_id',
$currentWorkspaceId)\n ->where('user_id', $user->id)\n
->where('account_status', UserFacebookAccount::STATUS_ACTIVE)\n
->where('ad_account_id', $adAccountId);\n\n // Filter by business ID
if provided\n if ($businessId) {\n $query-
>where('business_id', $businessId);\n }\n\n // Also
get accounts where optimization settings are NULL\n $query-
>where(function($q) {\n $q->whereNull('optimization')\n
->orWhereNull('roas_target')\n ->orWhereNull('spend_target');\
n });\n\n $accounts = $query->get();\n\n
if ($accounts->isEmpty()) {\n // Skip if no matching accounts
found\n continue;\n }\n\n foreach
($accounts as $account) {\n // Update optimization targets\n
$account->optimization = $optimization;\n $account->roas_target
= $roasTarget;\n $account->spend_target = $spendTarget;\n
$account->save();\n\n $updatedAccounts[] = $account;\n
}\n }\n\n // Commit transaction\n DB::commit();\n\
n return $updatedAccounts;\n } catch (Exception $e) {\n
// Roll back in case of error\n DB::rollBack();\n
Log::error('Error updating multiple Facebook optimization targets: ' . $e-
>getMessage());\n throw new FacebookApiException('Failed to update
multiple optimization targets: ' . $e->getMessage());\n }\n }\n\n /**\
n * Get ad accounts owned by the user's business (from the Facebook Business
API).\n * If business_id is provided, it will fetch accounts for that specific
business.\n *\n * @param Authenticatable $user\n * @param string
$businessId\n * @return array\n * @throws FacebookApiException\n */\n
public function getOwnedAdAccounts(Authenticatable $user, string $businessId):
array\n {\n try {\n $account = $this-
>getUserFacebookAccount($user);\n\n // Make a GET request to fetch owned
ad accounts for the business\n $client = new Client();\n
$response = $client->get('https://graph.facebook.com/' . $this->apiVersion . '/' .
$businessId . '/owned_ad_accounts', [\n 'query' => [\n
'access_token' => $account->access_token,\n 'fields' =>
'id,name,account_id,account_status,currency,timezone_name',\n ],\n
]);\n\n $data = json_decode((string)$response->getBody(), true);\n\n
if (!isset($data['data'])) {\n return [];\n }\n\n
return $data['data'];\n } catch (GuzzleException $e) {\n
Log::error('Facebook API request error: ' . $e->getMessage());\n $this-
>handleApiError($e, $user);\n throw new FacebookApiException('Error
fetching owned ad accounts: ' . $e->getMessage());\n } catch (Exception $e)
{\n if (!$e instanceof FacebookApiException) {\n
Log::error('Error fetching
owned Facebook ad accounts: ' . $e->getMessage());\n throw new
FacebookApiException('Failed to retrieve owned ad accounts: ' . $e->getMessage());\
n }\n throw $e;\n }\n }\n\n /**\n * Get
aggregated ad creatives from database grouped by creative (ad name) across all
dates\n *\n * @param Collection $adData Collection of FacebookAdData
models\n * @param float|null $roasTarget Optional ROAS target for win rate
calculation\n * @param float|null $spendTarget Optional spend target for win
rate calculation\n * @return array Aggregated creatives and optimization stats\
n */\n public function getAggregatedAdCreatives(Collection $adData, ?float
$roasTarget = null, ?float $spendTarget = null): array\n {\n // Group by
creative only (ignoring dates)\n $creativeGroups = $adData-
>groupBy('name');\n $aggregatedCreatives = [];\n
$winningCreativesCount = 0;\n $activeCreativesCount = 0;\n\n foreach
($creativeGroups as $creativeId => $creativeAds) {\n // Calculate date
range for this creative\n $startDate = $creativeAds->min('data_date');\n
$endDate = $creativeAds->max('data_date');\n $dateRange = $startDate;\n
if ($startDate !== $endDate) {\n $dateRange .= ' to ' . $endDate;\n
}\n\n // Process creative group and append date range\n
$creative = $this->processCreativeAdsGroup($creativeId, $creativeAds);\n
$creative['date_range'] = $dateRange;\n $creative['is_winning'] =
false;\n\n // Only include active ads in the win rate calculation\n
if ($creative['status'] === FacebookAdData::STATUS_ACTIVE && $creative['metrics']
['spend'] > 0) {\n $activeCreativesCount++;\n\n //
Check if this creative meets the optimization targets\n if
($roasTarget > 0 && $spendTarget > 0) {\n $meetsRoasTarget =
$creative['metrics']['roas'] >= $roasTarget;\n $meetsSpendTarget
= $creative['metrics']['spend'] >= $spendTarget;\n
$creative['is_winning'] = $meetsRoasTarget && $meetsSpendTarget;\n\n
if ($creative['is_winning']) {\n $winningCreativesCount++;\n
}\n }\n }\n\n $aggregatedCreatives[] =
$creative;\n }\n\n // Sort creatives by spend (highest first)\n
usort($aggregatedCreatives, function ($a, $b) {\n return $b['metrics']
['spend'] <=> $a['metrics']['spend'];\n });\n\n // Calculate overall
win rate using only active creatives\n $winRate = $activeCreativesCount >
0 ? round(($winningCreativesCount / $activeCreativesCount) * 100, 2) : 0;\n\n
return [\n 'creatives' => $aggregatedCreatives,\n
'optimization' => [\n 'win_rate' => $winRate,\n
'winning_creatives_count' => $winningCreativesCount,\n
'active_creatives_count' => $activeCreativesCount,\n
'total_creatives' => count($aggregatedCreatives)\n ]\n ];\n }\
n\n /**\n * Get aggregated ad creatives from database grouped by creative\n
*\n * @param Collection $adData Collection of FacebookAdData models\n *
@return array Aggregated creatives by date\n */\n public function
getAggregatedAdCreativesPerDate(Collection $adData): array\n {\n // Group
by date and creative\n $aggregatedByDate = [];\n $adData-
>groupBy('data_date')->each(function ($dateGroup, $date) use (&$aggregatedByDate)
{\n // Format date to Y-m-d without time component\n
$formattedDate = date('Y-m-d', strtotime($date));\n\n // Group this
date's ads by creative\n $creativeGroups = $dateGroup->groupBy('name');\
n\n $aggregatedCreatives = [];\n\n foreach ($creativeGroups
as $creativeId => $creativeAds) {\n // Process creative group\n
$creative = $this->processCreativeAdsGroup($creativeId, $creativeAds);\n
$aggregatedCreatives[] = $creative;\n }\n\n // Sort creatives
by spend (highest first)\n usort($aggregatedCreatives, function ($a, $b)
{\n return $b['metrics']['spend'] <=> $a['metrics']['spend'];\n
});\n\n $aggregatedByDate[$formattedDate] = $aggregatedCreatives;\n
});\n\n // Sort dates in ascending order\n ksort($aggregatedByDate);\
n\n return $aggregatedByDate;\n }\n\n /**\n * Parse date range
into Facebook time_range format.\n * Accepts either predefined range
identifiers like 'today', 'last_7d', etc.,\n * or a JSON-encoded string with
'since' and 'until' dates in Y-m-d format,\n * or an array with 'since' and
'until' keys.\n *\n * @param array|string $dateRange Date range identifier
or array with exact dates\n * @return array\n */\n protected function
parseDateRange(array|string $dateRange): array\n {\n // If $dateRange is
already an array with since/until keys, use it directly\n if
(is_array($dateRange) && isset($dateRange['since']) && isset($dateRange['until']))
{\n return [\n 'since' => $dateRange['since'],\n
'until' => $dateRange['until'],\n ];\n }\n\n // Check if
$dateRange is a JSON string with since/until\n if (is_string($dateRange) &&
$this->isJsonString($dateRange)) {\n $decoded = json_decode($dateRange,
true);\n if (isset($decoded['since']) && isset($decoded['until'])) {\n
return [\n 'since' => $decoded['since'],\n
'until' => $decoded['until'],\n ];\n }\n }\n\n
// Handle predefined date range identifiers\n switch ($dateRange) {\n
case 'today':\n $since = Carbon::today()->format('Y-m-d');\n
$until = Carbon::today()->format('Y-m-d');\n break;\n
case 'yesterday':\n $since = Carbon::yesterday()->format('Y-m-d');\n
$until = Carbon::yesterday()->format('Y-m-d');\n break;\n
case 'last_7d':\n $since = Carbon::today()->subDays(7)->format('Y-m-
d');\n $until = Carbon::yesterday()->format('Y-m-d');\n
break;\n case 'last_30d':\n $since = Carbon::today()-
>subDays(30)->format('Y-m-d');\n $until = Carbon::yesterday()-
>format('Y-m-d');\n break;\n case 'this_month':\n
$since = Carbon::today()->startOfMonth()->format('Y-m-d');\n $until
= Carbon::today()->format('Y-m-d');\n break;\n case
'last_month':\n $since = Carbon::today()->subMonth()-
>startOfMonth()->format('Y-m-d');\n $until = Carbon::today()-
>subMonth()->endOfMonth()->format('Y-m-d');\n break;\n
default:\n // Default to last 30 days\n $since =
Carbon::today()->subDays(30)->format('Y-m-d');\n $until =
Carbon::yesterday()->format('Y-m-d');\n break;\n }\n\n
return [\n 'since' => $since,\n 'until' => $until,\
n ];\n }\n\n /**\n * Check if a string is valid JSON\n *\n
* @param string $string\n * @return bool\n */\n private function
isJsonString(string $string): bool\n {\n json_decode($string);\n
return json_last_error() === JSON_ERROR_NONE;\n }\n\n /**\n * Extract
numeric value from Facebook's video metric array format\n *\n * @param
array $metricArray The Facebook video metric array\n * @return float The
extracted numeric value\n */\n private function
extractVideoMetricValue(array $metricArray): float\n {\n if
(empty($metricArray)) {\n return 0;\n }\n\n foreach
($metricArray as $action) {\n if (isset($action['action_type']) &&
$action['action_type'] === 'video_view') {\n if
(isset($action['value'])) {\n if (is_array($action['value'])) {\
n // We'll store the full data in the video_metrics_data
JSON field\n return 0;\n } else {\n
return (float)($action['value']);\n }\n }\n
break;\n }\n }\n\n return 0;\n }\n\n /**\n *
Process a group of creative ads to calculate metrics and build creative object\n
*\n * @param int|string $creativeId ID of the creative\n * @param
Collection $creativeAds Collection of ads with the same creative ID\n * @return
array Processed creative with aggregated metrics\n */\n private function
processCreativeAdsGroup(int|string $creativeId, Collection $creativeAds): array\n
{\n // Use the latest ad for basic details\n $latestAd =
$creativeAds->sortByDesc('updated_at')->first();\n\n // Aggregate metrics\n
$totalImpressions = $creativeAds->sum('impressions');\n $totalReach =
$creativeAds->sum('reach');\n $totalClicks = $creativeAds->sum('clicks');\n
$totalSpend = $creativeAds->sum('spend');\n $totalRevenue = $creativeAds-
>sum('revenue');\n $totalPurchases = $creativeAds->sum('purchases');\n\n
// Aggregate video metrics\n $total3SecViews = $creativeAds-
>sum('video_3sec_views');\n $totalP75Views = $creativeAds-
>sum('video_p75_views');\n\n // Calculate derived metrics based on
aggregated values (not averaging
pre-calculated values)\n $ctr = $totalImpressions > 0 ? ($totalClicks /
$totalImpressions) * 100 : 0;\n $cpm = $totalImpressions > 0 ?
($totalSpend / ($totalImpressions / 1000)) : 0;\n $cpp = $totalReach > 0 ?
($totalSpend / ($totalReach / 1000)) : 0;\n\n // Calculate ROAS,
profit/loss\n $roas = $totalSpend > 0 ? $totalRevenue / $totalSpend : 0;\n
$profitLoss = $totalRevenue - $totalSpend;\n\n // Calculate CPA if there are
purchases\n $cpa = ($totalPurchases > 0 && $totalSpend > 0) ? $totalSpend /
$totalPurchases : 0;\n\n // Calculate conversion rate (as a percentage)\n
$conversionRate = $totalImpressions > 0 ? ($totalPurchases / $totalImpressions) *
100 : 0;\n\n // Calculate video metrics\n $hookRate =
$totalImpressions > 0 ? ($total3SecViews / $totalImpressions) * 100 : 0;\n
$holdRate = $total3SecViews > 0 ? ($totalP75Views / $total3SecViews) * 100 : 0;\n
$costPer3SecView = $total3SecViews > 0 ? $totalSpend / $total3SecViews : 0;\n\n
// Cap rates to reasonable limits (same as in getAdInsights)\n if ($hookRate
> 100) $hookRate = 100; // Hook rate can't exceed 100%\n if ($holdRate >
100) $holdRate = 100; // Hold rate can't exceed 100%\n if ($costPer3SecView
< 0) $costPer3SecView = 0; // Cost can't be negative\n\n // Build the
creative object with both static and aggregated data\n return [\n
'id' => $creativeId,\n 'status' => $latestAd->status ??
FacebookAdData::STATUS_ACTIVE,\n 'name' => $latestAd->name ?? '',\n
'adset' => $latestAd->adset ?? '',\n 'adset_id' => $latestAd-
>adset_id ?? '',\n 'campaign' => $latestAd->campaign ?? '',\n
'campaign_id' => $latestAd->campaign_id ?? '',\n 'thumbnail' =>
$latestAd->thumbnail ?? '',\n 'original_image_url' => $latestAd-
>original_image_url ?? '',\n 'message' => $latestAd->message ?? '',\n
'call_to_action' => $latestAd->call_to_action ?? '',\n
'call_to_action_value' => $latestAd->call_to_action_value ?? '',\n
'effective_object_story_id' => $latestAd->effective_object_story_id ?? '',\n
'metrics' => [\n 'impressions' => $totalImpressions,\n
'reach' => $totalReach,\n 'clicks' => $totalClicks,\n
'spend' => round($totalSpend, 2),\n 'ctr' => round($ctr, 2),\n
'cpm' => round($cpm, 2),\n 'cpp' => round($cpp, 2),\n
'roas' => round($roas, 2),\n 'cpa' => round($cpa, 2),\n
'conversion_rate' => round($conversionRate, 2),\n 'revenue' =>
round($totalRevenue, 2),\n 'profit_loss' => round($profitLoss, 2),\n
'purchases' => $totalPurchases,\n 'video_3sec_views' =>
$total3SecViews,\n 'video_p75_views' => $totalP75Views,\n
'hook_rate' => round($hookRate, 2),\n 'hold_rate' =>
round($holdRate, 2),\n 'cost_per_3sec_view' =>
round($costPer3SecView, 2),\n ]\n ];\n }\n\n /**\n *
@param float|int $hookRate\n * @param float|int $holdRate\n * @param float|
int $conversionRate\n * @return bool\n */\n private function
isConversionRateWarningFlag(float|int $hookRate, float|int $holdRate, float|int
$conversionRate): bool\n {\n $conversionRateWarningFlag = false;\n
if ($hookRate > 0 && $holdRate > 0 && $conversionRate > 0) {\n
$expectedConversionRate = (($hookRate / 100) * ($holdRate / 100)) * 5;\n
// Flag if conversion rate is 2x higher than expected\n if
($conversionRate > ($expectedConversionRate * 2)) {\n
$conversionRateWarningFlag = true;\n }\n }\n\n return
$conversionRateWarningFlag;\n }\n\n /**\n * Calculate weighted average
for a metric across multiple ads\n * This ensures metrics are properly weighted
by impressions or another factor\n *\n * @param Collection $items
Collection of ad data\n * @return float The weighted average of the metric\n
*/\n private function calculateWeightedAverage(Collection $items): float\n {\
n $totalWeightedValue = 0;\n $totalWeight = 0;\n\n foreach
($items as $item) {\n // Use video_3sec_views as weight if impressions
is zero (for video-only views like Story ads)\n $weight =
($item['impressions'] ?? 0) > 0 ? ($item['impressions'] ?? 0) :
($item['video_3sec_views'] ?? 0);\n\n if ($weight > 0) {\n
$totalWeightedValue += ($item['video_avg_time_watched'] ?? 0) * $weight;\n
$totalWeight += $weight;\n }\n }\n\n return $totalWeight >
0 ? $totalWeightedValue / $totalWeight : 0;\n }\n\n /**\n * Store
Facebook ad data in the aggregated table\n *\n * @param UserFacebookAccount
$facebookAccount The user's Facebook account\n * @param string $adAccountId The
ad account ID\n * @param string $startDate Start date in Y-m-d format\n *
@param string $endDate End date in Y-m-d format\n * @param array $apiData The
ad data from the API\n * @return array The stored aggregated ads data\n */\
n public function storeAdsDataInAggregatedTable(UserFacebookAccount
$facebookAccount, string $adAccountId, string $startDate, string $endDate, array
$apiData): array\n {\n $storedAds = [];\n\n foreach ($apiData as
$adData) {\n // Skip if creative_id is missing\n if
(empty($adData['creative_id'])) {\n continue;\n }\n\n
// Prepare ad data record\n $adRecord = [\n
'user_facebook_account_id' => $facebookAccount->id,\n
'ad_account_id' => $adAccountId,\n 'start_date' => $startDate,\n
'end_date' => $endDate,\n 'facebook_ad_id' => $adData['id'] ??
null,\n 'name' => $adData['name'] ?? null,\n 'status'
=> $adData['status'] ?? 'UNKNOWN',\n 'adset' => $adData['adset'] ??
null,\n 'adset_id' => $adData['adset_id'] ?? null,\n
'campaign' => $adData['campaign'] ?? null,\n 'campaign_id' =>
$adData['campaign_id'] ?? null,\n 'creative_id' =>
$adData['creative_id'],\n 'video_id' => $adData['video_id'] ??
null,\n 'thumbnail' => $adData['thumbnail'] ?? null,\n
'original_image_url' => $adData['original_image_url'] ?? null,\n
'message' => $adData['message'] ?? null,\n 'call_to_action' =>
$adData['call_to_action'] ?? null,\n 'call_to_action_value' =>
$adData['call_to_action_value'] ?? null,\n
'effective_object_story_id' => $adData['effective_object_story_id'] ?? null,\n
];\n\n // Add metrics if available\n if
(isset($adData['metrics']) && is_array($adData['metrics'])) {\n
$metrics = $adData['metrics'];\n $adRecord = array_merge($adRecord,
[\n 'impressions' => $metrics['impressions'] ?? 0,\n
'spend' => $metrics['spend'] ?? 0,\n 'clicks' =>
$metrics['clicks'] ?? 0,\n 'cpc' => $metrics['cpc'] ?? 0,\n
'cpm' => $metrics['cpm'] ?? 0,\n 'ctr' => $metrics['ctr'] ?? 0,\
n 'frequency' => $metrics['frequency'] ?? 0,\n
'reach' => $metrics['reach'] ?? 0,\n 'inline_link_clicks' =>
$metrics['inline_link_clicks'] ?? 0,\n
'cost_per_inline_link_click' => $metrics['cost_per_inline_link_click'] ?? 0,\n
'roas' => $metrics['roas'] ?? 0,\n 'cpa' => $metrics['cpa'] ??
0,\n 'conversion_rate' => $metrics['conversion_rate'] ?? 0,\n
'revenue' => $metrics['revenue'] ?? 0,\n 'profit_loss' =>
$metrics['profit_loss'] ?? 0,\n 'purchases' =>
$metrics['purchases'] ?? 0,\n\n // Include video metrics\n
'video_3sec_views' => $metrics['video_3sec_views'] ?? 0,\n
'video_p75_views' => $metrics['video_p75_views'] ?? 0,\n
'hook_rate' => $metrics['hook_rate'] ?? 0,\n 'hold_rate' =>
$metrics['hold_rate'] ?? 0,\n 'cost_per_3sec_view' =>
$metrics['cost_per_3sec_view'] ?? 0,\n
'conversion_rate_warning_flag' => $metrics['conversion_rate_warning_flag'] ??
false,\n 'video_metrics_data' =>
$metrics['video_metrics_data'] ?? null,\n ]);\n }\n\n
// Encode JSON fields\n if (isset($adData['actions'])) {\n
$adRecord['actions'] = json_encode($adData['actions']);\n }\n\n
if (isset($adData['action_values'])) {\n $adRecord['action_values']
= json_encode($adData['action_values']);\n }\n\n if
(isset($adData['cost_per_action_type'])) {\n
$adRecord['cost_per_action_type'] = json_encode($adData['cost_per_action_type']);\n
}\n\n // Create or update the record in a single query\n
$storedAd = FacebookAdDataAggregated::updateOrCreate(\n [\n
'user_facebook_account_id' => $facebookAccount->id,\n
'ad_account_id' => $adAccountId,\n 'start_date' => $startDate,\n
'end_date' => $endDate,\n 'facebook_ad_id' => $adData['id'] ??
null,\n ],\n $adRecord\n );\n\n
$storedAds[] = $storedAd;\n }\n\n return $storedAds;\n
}\n}\n",
"prFileDiff" : "diff --git a/FacebookAdServiceOld.php
b/FacebookAdServiceOld.php\nindex c1c4422..1ede7c2 100644\n---
a/FacebookAdServiceOld.php\n+++ b/FacebookAdServiceOld.php\n@@ -65,6 +65,7 @@ class
FacebookAdService\n 'business_management',\n
'pages_read_engagement',\n 'pages_show_list',\n+
'pages_show_list',\n ];\n \n // Generate state parameter
for CSRF protection",
"prFileDiffHunks" : [
"@@ -65,6 +65,7 @@ class FacebookAdService\n
'business_management',\n 'pages_read_engagement',\n
'pages_show_list',\n+ 'pages_show_list',\n ];\n \n
// Generate state parameter for CSRF protection"
],
"prFileBlobUrl" :
"https://api.bitbucket.org/2.0/repositories/inovixpro/test/src/58301c86eda437b5b49f
c0c9b09bd814931072d4/FacebookAdServiceOld.php",
"_id" : ObjectId("68b9c28311c19ccd7763600a")
}
],
"issueCount" : null,
"createdAt" : ISODate("2025-09-04T16:46:59.528+0000"),
"updatedAt" : ISODate("2025-09-04T16:46:59.528+0000")
}

You might also like