baseUrl = config('trypost.platforms.pinterest.api'); } private string $accessToken; public function getMetrics(SocialAccount $account, ?CarbonInterface $since = null, ?CarbonInterface $until = null): array { $since ??= now()->subDays(7); $until ??= now(); $cacheKey = "analytics:pinterest:{$account->id}:{$since->format('Y-m-d')}:{$until->format('Y-m-d')}"; $cacheTtl = app()->isProduction() ? 3600 : 1; return Cache::remember($cacheKey, $cacheTtl, function () use ($account, $since, $until) { return $this->fetchMetricsFromApi($account, $since, $until); }); } public function fetchPostMetrics(PostPlatform $postPlatform): array { $account = $postPlatform->socialAccount; if (! $account || ! $postPlatform->platform_post_id) { return ['unsupported' => true, 'reason' => 'missing_post_id']; } if ($account->is_token_expired || $account->is_token_expiring_soon) { $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); $account->refresh(); } $start = now()->subDays(90)->format('Y-m-d'); $end = now()->format('Y-m-d'); $response = $this->socialHttp() ->withToken($account->access_token) ->get("{$this->baseUrl}/pins/{$postPlatform->platform_post_id}/analytics", [ 'start_date' => $start, 'end_date' => $end, 'metric_types' => 'IMPRESSION,SAVE,PIN_CLICK,OUTBOUND_CLICK,VIDEO_MRC_VIEW', ]); if ($response->failed()) { Log::warning('Pinterest post metrics fetch failed', [ 'body' => $this->redactResponseBody($response->body()), ]); return ['unsupported' => true, 'reason' => 'api_error']; } $data = data_get($response->json(), 'all.summary_metrics', []); $rows = [ 'IMPRESSION' => 'Impressions', 'SAVE' => 'Saves', 'PIN_CLICK' => 'Pin clicks', 'OUTBOUND_CLICK' => 'Outbound clicks', 'VIDEO_MRC_VIEW' => 'Video views', ]; return collect($rows) ->map(fn (string $label, string $key) => [ 'label' => $label, 'value' => (int) data_get($data, $key, 0), ]) ->values() ->all(); } private function fetchMetricsFromApi(SocialAccount $account, CarbonInterface $since, CarbonInterface $until): array { if ($account->is_token_expired || $account->is_token_expiring_soon) { $this->refreshTokenWithLock($account, fn () => $this->refreshToken($account)); $account->refresh(); } $this->accessToken = $account->access_token; $response = $this->getHttpClient() ->get("{$this->baseUrl}/user_account/analytics", [ 'start_date' => $since->format('Y-m-d'), 'end_date' => $until->format('Y-m-d'), ]); if ($response->failed()) { Log::warning('Pinterest analytics fetch failed', [ 'body' => $this->redactResponseBody($response->body()), ]); return []; } $dailyMetrics = data_get($response->json(), 'all.daily_metrics', []); if (empty($dailyMetrics)) { return []; } $totals = [ 'PIN_CLICK_RATE' => 0, 'IMPRESSION' => 0, 'PIN_CLICK' => 0, 'ENGAGEMENT' => 0, 'SAVE' => 0, ]; $count = 0; foreach ($dailyMetrics as $day) { $metrics = data_get($day, 'metrics', []); if (! isset($metrics['PIN_CLICK_RATE'])) { continue; } $count++; $totals['PIN_CLICK_RATE'] += $metrics['PIN_CLICK_RATE']; $totals['IMPRESSION'] += $metrics['IMPRESSION'] ?? 0; $totals['PIN_CLICK'] += $metrics['PIN_CLICK'] ?? 0; $totals['ENGAGEMENT'] += $metrics['ENGAGEMENT'] ?? 0; $totals['SAVE'] += $metrics['SAVE'] ?? 0; } $avgClickRate = $count > 0 ? round($totals['PIN_CLICK_RATE'] / $count, 4) : 0; return [ ['label' => 'Impressions', 'value' => $totals['IMPRESSION']], ['label' => 'Pin Clicks', 'value' => $totals['PIN_CLICK']], ['label' => 'Engagement', 'value' => $totals['ENGAGEMENT']], ['label' => 'Saves', 'value' => $totals['SAVE']], ['label' => 'Pin Click Rate', 'value' => $avgClickRate], ]; } private function getHttpClient(): PendingRequest { return $this->socialHttp()->withToken($this->accessToken); } private function refreshToken(SocialAccount $account): void { if (! $account->refresh_token) { throw new TokenExpiredException('No refresh token available for Pinterest account'); } $response = Http::withBasicAuth( config('services.pinterest.client_id'), config('services.pinterest.client_secret'), )->asForm()->post(config('trypost.platforms.pinterest.api').'/oauth/token', [ 'grant_type' => 'refresh_token', 'refresh_token' => $account->refresh_token, ]); if ($response->failed()) { Log::error('Pinterest token refresh failed', ['body' => $this->redactResponseBody($response->body())]); throw new TokenExpiredException('Pinterest token refresh failed'); } $data = $response->json(); $account->update([ 'access_token' => data_get($data, 'access_token'), 'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token), 'token_expires_at' => data_get($data, 'expires_in') ? now()->addSeconds(data_get($data, 'expires_in')) : null, ]); } }