$headers, 'rows' => [ [ 'cells' => array_map(fn (string $value): array => ['value' => $value], $rowValues), ], ], ]; } it('maps report cells by header name rather than position', function (): void { $parser = new AdsenseReportParser; $report = adsenseReport([ ['name' => 'ESTIMATED_EARNINGS', 'type' => 'METRIC_CURRENCY', 'currencyCode' => 'EUR'], ['name' => 'DATE', 'type' => 'DIMENSION'], ['name' => 'PAGE_VIEWS', 'type' => 'METRIC_TALLY'], ['name' => 'PAGE_VIEWS_CTR', 'type' => 'METRIC_RATIO'], ], ['12.50', '2026-09-18', '1000', '0.0125']); $rows = $parser->parse($report, AdsenseDailyStat::DIMENSION_TOTAL, 'accounts/pub-1'); expect($rows)->toHaveCount(1) ->and($rows[0]['date'])->toBe('2026-09-18') ->and($rows[0]['dimension_key'])->toBe(AdsenseDailyStat::TOTAL_DIMENSION_KEY) ->and($rows[0]['estimated_earnings'])->toBe('12.50') ->and($rows[0]['page_views'])->toBe(1000) ->and($rows[0]['page_views_ctr'])->toBe('0.0125') ->and($rows[0]['currency_code'])->toBe('EUR'); }); it('parses the same values when Google returns columns in another order', function (): void { $parser = new AdsenseReportParser; $first = $parser->parse(adsenseReport([ ['name' => 'DATE', 'type' => 'DIMENSION'], ['name' => 'AD_UNIT_ID', 'type' => 'DIMENSION'], ['name' => 'CLICKS', 'type' => 'METRIC_TALLY'], ['name' => 'ESTIMATED_EARNINGS', 'type' => 'METRIC_CURRENCY', 'currencyCode' => 'USD'], ], ['2026-09-18', 'unit-1', '8', '4.25']), AdsenseDailyStat::DIMENSION_AD_UNIT, 'accounts/pub-1'); $second = $parser->parse(adsenseReport([ ['name' => 'ESTIMATED_EARNINGS', 'type' => 'METRIC_CURRENCY', 'currencyCode' => 'USD'], ['name' => 'CLICKS', 'type' => 'METRIC_TALLY'], ['name' => 'AD_UNIT_ID', 'type' => 'DIMENSION'], ['name' => 'DATE', 'type' => 'DIMENSION'], ], ['4.25', '8', 'unit-1', '2026-09-18']), AdsenseDailyStat::DIMENSION_AD_UNIT, 'accounts/pub-1'); expect($first[0]['dimension_key'])->toBe('unit-1') ->and($first[0]['clicks'])->toBe($second[0]['clicks']) ->and($first[0]['estimated_earnings'])->toBe($second[0]['estimated_earnings']) ->and($first[0]['date'])->toBe($second[0]['date']); }); it('detects monetary currency from metric headers', function (): void { $parser = new AdsenseReportParser; $code = $parser->currencyCode([ ['name' => 'DATE', 'type' => 'DIMENSION'], ['name' => 'ESTIMATED_EARNINGS', 'type' => 'METRIC_CURRENCY', 'currencyCode' => 'GBP'], ]); expect($code)->toBe('GBP'); }); it('keeps ratio metrics as google decimals', function (): void { $parser = new AdsenseReportParser; $rows = $parser->parse(adsenseReport([ ['name' => 'DATE', 'type' => 'DIMENSION'], ['name' => 'PLATFORM_TYPE_CODE', 'type' => 'DIMENSION'], ['name' => 'AD_REQUESTS_COVERAGE', 'type' => 'METRIC_RATIO'], ['name' => 'IMPRESSIONS_CTR', 'type' => 'METRIC_RATIO'], ], ['2026-09-19', 'DESKTOP', '0.84', '0.031']), AdsenseDailyStat::DIMENSION_PLATFORM, 'accounts/pub-1'); expect($rows[0]['ad_requests_coverage'])->toBe('0.84') ->and($rows[0]['impressions_ctr'])->toBe('0.031') ->and($rows[0]['dimension_key'])->toBe('DESKTOP'); }); it('parses country and custom channel dimension keys', function (): void { $parser = new AdsenseReportParser; $country = $parser->parse(adsenseReport([ ['name' => 'COUNTRY_CODE', 'type' => 'DIMENSION'], ['name' => 'DATE', 'type' => 'DIMENSION'], ['name' => 'PAGE_VIEWS', 'type' => 'METRIC_TALLY'], ], ['SI', '2026-09-19', '12']), AdsenseDailyStat::DIMENSION_COUNTRY, 'accounts/pub-1'); $channel = $parser->parse(adsenseReport([ ['name' => 'DATE', 'type' => 'DIMENSION'], ['name' => 'CUSTOM_CHANNEL_ID', 'type' => 'DIMENSION'], ['name' => 'CLICKS', 'type' => 'METRIC_TALLY'], ], ['2026-09-19', 'ch-9', '3']), AdsenseDailyStat::DIMENSION_CUSTOM_CHANNEL, 'accounts/pub-1'); expect($country[0]['dimension_key'])->toBe('SI') ->and($channel[0]['dimension_key'])->toBe('ch-9'); });