startOfDay(); [$start, $end] = match ($period) { 'today' => [$today->copy(), $today->copy()], 'yesterday' => [$today->copy()->subDay(), $today->copy()->subDay()], '7d' => [$today->copy()->subDays(6), $today->copy()], 'custom' => [ Carbon::parse($from ?: $today->copy()->subDays(29)->toDateString())->startOfDay(), Carbon::parse($to ?: $today->toDateString())->startOfDay(), ], default => [$today->copy()->subDays(29), $today->copy()], }; if ($end->lt($start)) { [$start, $end] = [$end, $start]; } $days = $start->diffInDays($end) + 1; $previousEnd = $start->copy()->subDay(); $previousStart = $previousEnd->copy()->subDays($days - 1); return [ 'from' => $start, 'to' => $end, 'previous_from' => $previousStart, 'previous_to' => $previousEnd, 'period' => $period === 'custom' ? 'custom' : (in_array($period, ['today', 'yesterday', '7d', '30d'], true) ? $period : '30d'), ]; } /** * @return array */ public function dashboard(AdsenseConnection $connection, Carbon $from, Carbon $to, Carbon $previousFrom, Carbon $previousTo): array { $account = (string) $connection->account_resource_name; $current = $this->totals($account, $from, $to); $previous = $this->totals($account, $previousFrom, $previousTo); $currency = $current['currency_code'] ?? $previous['currency_code'] ?? 'USD'; return [ 'kpis' => [ $this->kpi('estimated_earnings', 'Estimated Earnings', $current['estimated_earnings'], $previous['estimated_earnings'], 'money', $currency), $this->kpi('page_views', 'Page Views', $current['page_views'], $previous['page_views'], 'integer'), $this->kpi('impressions', 'Ad Impressions', $current['impressions'], $previous['impressions'], 'integer'), $this->kpi('clicks', 'Clicks', $current['clicks'], $previous['clicks'], 'integer'), $this->kpi('page_views_ctr', 'Page CTR', $this->ratio($current['clicks'], $current['page_views']), $this->ratio($previous['clicks'], $previous['page_views']), 'percent'), $this->kpi('page_views_rpm', 'Page RPM', $this->rpm($current['estimated_earnings'], $current['page_views']), $this->rpm($previous['estimated_earnings'], $previous['page_views']), 'money', $currency), $this->kpi('ad_requests_coverage', 'Ad Request Coverage', $this->ratio($current['matched_ad_requests'], $current['ad_requests']), $this->ratio($previous['matched_ad_requests'], $previous['ad_requests']), 'percent'), $this->kpi('cost_per_click', 'CPC', $this->cpc($current['estimated_earnings'], $current['clicks']), $this->cpc($previous['estimated_earnings'], $previous['clicks']), 'money', $currency), ], 'chart' => $this->chart($account, $from, $to), 'placements' => $this->dimensionTable($account, AdsenseDailyStat::DIMENSION_AD_UNIT, $from, $to, $currency), 'platforms' => $this->dimensionTable($account, AdsenseDailyStat::DIMENSION_PLATFORM, $from, $to, $currency), 'countries' => array_slice($this->dimensionTable($account, AdsenseDailyStat::DIMENSION_COUNTRY, $from, $to, $currency), 0, 20), 'currency_code' => $currency, ]; } /** * @return array */ private function totals(string $account, Carbon $from, Carbon $to): array { $row = AdsenseDailyStat::query() ->where('account_resource_name', $account) ->where('dimension_type', AdsenseDailyStat::DIMENSION_TOTAL) ->whereDate('date', '>=', $from->toDateString()) ->whereDate('date', '<=', $to->toDateString()) ->selectRaw(' SUM(page_views) as page_views, SUM(ad_requests) as ad_requests, SUM(matched_ad_requests) as matched_ad_requests, SUM(impressions) as impressions, SUM(clicks) as clicks, SUM(estimated_earnings) as estimated_earnings, MAX(currency_code) as currency_code ') ->first(); return [ 'page_views' => (int) ($row?->page_views ?? 0), 'ad_requests' => (int) ($row?->ad_requests ?? 0), 'matched_ad_requests' => (int) ($row?->matched_ad_requests ?? 0), 'impressions' => (int) ($row?->impressions ?? 0), 'clicks' => (int) ($row?->clicks ?? 0), 'estimated_earnings' => (string) ($row?->estimated_earnings ?? '0'), 'currency_code' => $row?->currency_code, ]; } /** * @return list> */ private function chart(string $account, Carbon $from, Carbon $to): array { $rows = AdsenseDailyStat::query() ->where('account_resource_name', $account) ->where('dimension_type', AdsenseDailyStat::DIMENSION_TOTAL) ->whereDate('date', '>=', $from->toDateString()) ->whereDate('date', '<=', $to->toDateString()) ->orderBy('date') ->get(); return $rows->map(function (AdsenseDailyStat $row): array { return [ 'date' => optional($row->date)?->toDateString(), 'estimated_earnings' => (float) $row->estimated_earnings, 'page_views' => (int) $row->page_views, 'page_views_rpm' => (float) $row->page_views_rpm, 'clicks' => (int) $row->clicks, ]; })->values()->all(); } /** * @return list> */ private function dimensionTable(string $account, string $dimensionType, Carbon $from, Carbon $to, ?string $currency): array { $rows = AdsenseDailyStat::query() ->where('account_resource_name', $account) ->where('dimension_type', $dimensionType) ->whereDate('date', '>=', $from->toDateString()) ->whereDate('date', '<=', $to->toDateString()) ->selectRaw(' dimension_key, MAX(dimension_label) as dimension_label, SUM(page_views) as page_views, SUM(impressions) as impressions, SUM(clicks) as clicks, SUM(estimated_earnings) as estimated_earnings ') ->groupBy('dimension_key') ->orderByDesc('estimated_earnings') ->get(); $labels = $this->entityLabels($account, $dimensionType); $countryNames = $dimensionType === AdsenseDailyStat::DIMENSION_COUNTRY ? $this->countryNames() : collect(); return $rows->map(function ($row) use ($labels, $countryNames, $currency, $dimensionType): array { $key = (string) $row->dimension_key; $label = (string) ($labels[$key] ?? $countryNames[$key] ?? $row->dimension_label ?? $key); if ($dimensionType === AdsenseDailyStat::DIMENSION_PLATFORM) { $label = $this->platformLabel($key, $label); } $pageViews = (int) $row->page_views; $impressions = (int) $row->impressions; $clicks = (int) $row->clicks; $earnings = (string) ($row->estimated_earnings ?? '0'); return [ 'key' => $key, 'label' => $label, 'page_views' => $pageViews, 'impressions' => $impressions, 'clicks' => $clicks, 'ctr' => $this->ratio($clicks, $pageViews), 'rpm' => $this->rpm($earnings, $pageViews), 'revenue' => $earnings, 'currency_code' => $currency, ]; })->values()->all(); } /** * @return array */ private function entityLabels(string $account, string $dimensionType): array { if (! in_array($dimensionType, [AdsenseDailyStat::DIMENSION_AD_UNIT, AdsenseDailyStat::DIMENSION_CUSTOM_CHANNEL], true)) { return []; } return AdsenseEntity::query() ->where('account_resource_name', $account) ->where('type', $dimensionType) ->pluck('display_name', 'reporting_dimension_id') ->all(); } /** * @return Collection */ private function countryNames(): Collection { if (! Schema::hasTable('countries')) { return collect(); } return Country::query() ->get(['iso2', 'name', 'name_common']) ->mapWithKeys(function (Country $country): array { $code = strtoupper((string) $country->iso2); $name = (string) ($country->name_common ?: $country->name ?: $code); return $code !== '' ? [$code => $name] : []; }); } private function platformLabel(string $key, string $fallback): string { return match (strtoupper($key)) { 'DESKTOP' => 'Desktop', 'HIGH_END_MOBILE', 'MOBILE' => 'Mobile', 'TABLET' => 'Tablet', default => $fallback !== '' ? $fallback : $key, }; } /** * @return array */ private function kpi(string $key, string $label, mixed $current, mixed $previous, string $format, ?string $currency = null): array { return [ 'key' => $key, 'label' => $label, 'value' => $current, 'previous' => $previous, 'change_percent' => $this->changePercent($current, $previous), 'format' => $format, 'currency_code' => $currency, ]; } private function changePercent(mixed $current, mixed $previous): ?float { $current = (float) $current; $previous = (float) $previous; if ($previous == 0.0) { return null; } return (($current - $previous) / $previous) * 100; } private function ratio(int $numerator, int $denominator): ?float { if ($denominator === 0) { return null; } return $numerator / $denominator; } private function rpm(string|float|int|null $earnings, int $pageViews): ?float { if ($pageViews === 0) { return null; } return ((float) $earnings / $pageViews) * 1000; } private function cpc(string|float|int|null $earnings, int $clicks): ?float { if ($clicks === 0) { return null; } return (float) $earnings / $clicks; } }