output:
Chunk ID: 75eb5a
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1399
Output:
<?php

namespace App\Services;

use App\Enums\AnalyticsEventType;
use App\Models\AnalyticsDaily;
use App\Models\AnalyticsEvent;
use App\Models\Store;
use Carbon\CarbonInterface;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

class AnalyticsService
{
    /**
     * @param  array<string, mixed>  $properties
     */
    public function track(
        Store $store,
        string $type,
        array $properties = [],
        ?string $sessionId = null,
        ?int $customerId = null,
        ?string $clientEventId = null,
        ?CarbonInterface $occurredAt = null,
    ): bool {
        if ($clientEventId !== null && AnalyticsEvent::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->where('client_event_id', $clientEventId)
            ->exists()) {
            return false;
        }

        AnalyticsEvent::withoutGlobalScopes()->create([
            'store_id' => $store->getKey(),
            'type' => AnalyticsEventType::from($type),
            'session_id' => $sessionId,
            'customer_id' => $customerId,
            'properties_json' => $properties,
            'client_event_id' => $clientEventId,
            'occurred_at' => $occurredAt ?? now(),
            'created_at' => now(),
        ]);

        return true;
    }

    /**
     * @param  list<array<string, mixed>>  $events
     * @return array{accepted: int, rejected: int}
     */
    public function trackBatch(Store $store, array $events): array
    {
        $accepted = 0;
        $rejected = 0;

        foreach ($events as $event) {
            $tracked = $this->track(
                $store,
                (string) $event['type'],
                $event['properties'] ?? [],
                $event['session_id'] ?? null,
                $event['customer_id'] ?? null,
                $event['client_event_id'] ?? null,
                isset($event['occurred_at']) ? Carbon::parse($event['occurred_at']) : null,
            );

            $tracked ? $accepted++ : $rejected++;
        }

        return [
            'accepted' => $accepted,
            'rejected' => $rejected,
        ];
    }

    public function getDailyMetrics(Store $store, string $startDate, string $endDate): Collection
    {
        return AnalyticsDaily::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->whereBetween('date', [$startDate, $endDate])
            ->orderBy('date')
            ->get();
    }

    public function aggregate(?CarbonInterface $date = null): int
    {
        $date = ($date ? Carbon::parse($date->toDateString(), 'UTC') : now('UTC')->subDay())->startOfDay();
        $start = $date->copy()->startOfDay();
        $end = $date->copy()->endOfDay();
        $dateString = $date->toDateString();

        $storeIds = AnalyticsEvent::withoutGlobalScopes()
            ->whereBetween('occurred_at', [$start, $end])
            ->distinct()
            ->pluck('store_id');

        $rows = 0;

        foreach ($storeIds as $storeId) {
            $events = AnalyticsEvent::withoutGlobalScopes()
                ->where('store_id', $storeId)
                ->whereBetween('occurred_at', [$start, $end])
                ->get();

            $checkoutCompleted = $events->where('type', AnalyticsEventType::CheckoutCompleted);
            $ordersCount = $checkoutCompleted->count();
            $revenue = (int) $checkoutCompleted->sum(fn (AnalyticsEvent $event): int => (int) data_get($event->properties_json, 'total_amount', 0));

            DB::table('analytics_daily')->updateOrInsert(
                [
                    'store_id' => (int) $storeId,
                    'date' => $dateString,
                ],
                [
                    'orders_count' => $ordersCount,
                    'revenue_amount' => $revenue,
                    'aov_amount' => $ordersCount > 0 ? intdiv($revenue, $ordersCount) : 0,
                    'visits_count' => $events
                        ->where('type', AnalyticsEventType::PageView)
                        ->pluck('session_id')
                        ->filter()
                        ->unique()
                        ->count(),
                    'add_to_cart_count' => $events->where('type', AnalyticsEventType::AddToCart)->count(),
                    'checkout_started_count' => $events->where('type', AnalyticsEventType::CheckoutStarted)->count(),
                    'checkout_completed_count' => $ordersCount,
                ],
            );

            $rows++;
        }

        return $rows;
    }

    /**
     * @return array{orders_count: int, revenue_amount: int, aov_amount: int, visits_count: int, add_to_cart_count: int, checkout_started_count: int, checkout_completed_count: int}
     */
    public function totals(Store $store, string $startDate, string $endDate): array
    {
        $metrics = $this->getDailyMetrics($store, $startDate, $endDate);
        $orders = (int) $metrics->sum('orders_count');
        $revenue = (int) $metrics->sum('revenue_amount');

        return [
            'orders_count' => $orders,
            'revenue_amount' => $revenue,
            'aov_amount' => $orders > 0 ? intdiv($revenue, $orders) : 0,
            'visits_count' => (int) $metrics->sum('visits_count'),
            'add_to_cart_count' => (int) $metrics->sum('add_to_cart_count'),
            'checkout_started_count' => (int) $metrics->sum('checkout_started_count'),
            'checkout_completed_count' => (int) $metrics->sum('checkout_completed_count'),
        ];
    }
}


output:
Chunk ID: 435059
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2343
Output:
<?php

namespace App\Livewire\Admin\Analytics;

use App\Enums\AnalyticsEventType;
use App\Enums\FinancialStatus;
use App\Enums\StoreUserRole;
use App\Models\AnalyticsEvent;
use App\Models\OrderLine;
use App\Models\Store;
use App\Services\AnalyticsService;
use App\Support\Money;
use Carbon\CarbonInterface;
use Carbon\CarbonPeriod;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Locked;
use Livewire\Component;
use Throwable;

class Index extends Component
{
    use AuthorizesRequests;

    #[Locked]
    public int $storeId;

    public string $storeCurrency = 'EUR';

    public string $dateRange = 'last_30_days';

    public ?string $customStartDate = null;

    public ?string $customEndDate = null;

    public string $channelFilter = 'all';

    public string $deviceFilter = 'all';

    public int $totalSales = 0;

    public int $ordersCount = 0;

    public int $averageOrderValue = 0;

    public float $conversionRate = 0.0;

    /**
     * @var list<array{date: string, label: string, revenue: int, orders: int}>
     */
    public array $salesChartData = [];

    public int $maxSalesChartAmount = 1;

    /**
     * @var list<array{title: string, units_sold: int, revenue: int, percentage: float}>
     */
    public array $topProducts = [];

    /**
     * @var list<array{source: string, sessions: int, orders: int, conversion_rate: float}>
     */
    public array $topReferrers = [];

    public bool $isExporting = false;

    public ?string $exportUrl = null;

    public function mount(AnalyticsService $analytics): void
    {
        $store = $this->store();

        $this->authorize('view', $store);
        abort_unless($this->canViewAnalytics($store), 403);

        $this->storeId = $store->getKey();
        $this->storeCurrency = $store->default_currency;

        $this->loadAnalytics($analytics);
    }

    public function updatedDateRange(AnalyticsService $analytics): void
    {
        $this->loadAnalytics($analytics);
    }

    public function updatedCustomStartDate(AnalyticsService $analytics): void
    {
        if ($this->dateRange === 'custom') {
            $this->loadAnalytics($analytics);
        }
    }

    public function updatedCustomEndDate(AnalyticsService $analytics): void
    {
        if ($this->dateRange === 'custom') {
            $this->loadAnalytics($analytics);
        }
    }

    public function updatedChannelFilter(AnalyticsService $analytics): void
    {
        $this->loadAnalytics($analytics);
    }

    public function updatedDeviceFilter(AnalyticsService $analytics): void
    {
        $this->loadAnalytics($analytics);
    }

    public function loadAnalytics(AnalyticsService $analytics): void
    {
        [$start, $end] = $this->currentRange();
        $store = $this->scopedStore();

        $totals = $analytics->totals($store, $start->toDateString(), $end->toDateString());

        $this->totalSales = $totals['revenue_amount'];
        $this->ordersCount = $totals['orders_count'];
        $this->averageOrderValue = $totals['aov_amount'];
        $this->conversionRate = $totals['visits_count'] > 0
            ? round(($totals['checkout_completed_count'] / $totals['visits_count']) * 100, 2)
            : 0.0;

        $daily = $analytics->getDailyMetrics($store, $start->toDateString(), $end->toDateString())->keyBy('date');

        $this->salesChartData = collect(CarbonPeriod::create($start->copy()->startOfDay(), '1 day', $end->copy()->startOfDay()))
            ->map(function (CarbonInterface $date) use ($daily): array {
                $metric = $daily->get($date->toDateString());

                return [
                    'date' => $date->toDateString(),
                    'label' => $date->format('M j'),
                    'revenue' => (int) ($metric?->revenue_amount ?? 0),
                    'orders' => (int) ($metric?->orders_count ?? 0),
                ];
            })
            ->values()
            ->all();

        $this->maxSalesChartAmount = max(1, max(array_column($this->salesChartData, 'revenue') ?: [0]));
        $this->loadTopProducts($start, $end);
        $this->loadTopReferrers($start, $end);
    }

    public function exportCsv(AnalyticsService $analytics): void
    {
        $this->isExporting = true;
        $this->loadAnalytics($analytics);

        $lines = ['date,revenue_amount,orders_count'];

        foreach ($this->salesChartData as $point) {
            $lines[] = "{$point['date']},{$point['revenue']},{$point['orders']}";
        }

        $this->exportUrl = 'data:text/csv;charset=utf-8,'.rawurlencode(implode("\n", $lines));
        $this->isExporting = false;
    }

    #[Computed]
    public function formattedTotalSales(): string
    {
        return Money::format($this->totalSales, $this->storeCurrency);
    }

    #[Computed]
    public function formattedAov(): string
    {
        return Money::format($this->averageOrderValue, $this->storeCurrency);
    }

    public function render(): mixed
    {
        return view('livewire.admin.analytics.index')->layout('layouts.app', [
            'title' => __('Analytics'),
        ]);
    }

    private function loadTopProducts(CarbonInterface $start, CarbonInterface $end): void
    {
        $rows = OrderLine::query()
            ->selectRaw('order_lines.title_snapshot as title, sum(order_lines.quantity) as units_sold, sum(order_lines.total_amount) as revenue')
            ->join('orders', 'orders.id', '=', 'order_lines.order_id')
            ->where('orders.store_id', $this->storeId)
            ->whereBetween('orders.placed_at', [$start, $end])
            ->whereIn('orders.financial_status', [
                FinancialStatus::Paid->value,
                FinancialStatus::PartiallyRefunded->value,
            ])
            ->groupBy('order_lines.product_id', 'order_lines.title_snapshot')
            ->orderByDesc('revenue')
            ->limit(10)
            ->get();

        $totalRevenue = max(1, (int) $rows->sum('revenue'));

        $this->topProducts = $rows
            ->map(fn (OrderLine $line): array => [
                'title' => (string) $line->title,
                'units_sold' => (int) $line->units_sold,
                'revenue' => (int) $line->revenue,
                'percentage' => round(((int) $line->revenue / $totalRevenue) * 100, 1),
            ])
            ->all();
    }

    private function loadTopReferrers(CarbonInterface $start, CarbonInterface $end): void
    {
        $pageViews = $this->filteredEvents($start, $end, AnalyticsEventType::PageView);
        $orders = $this->filteredEvents($start, $end, AnalyticsEventType::CheckoutCompleted);
        $ordersBySource = $orders->groupBy(fn (AnalyticsEvent $event): string => $this->sourceFor($event));

        $this->topReferrers = $pageViews
            ->groupBy(fn (AnalyticsEvent $event): string => $this->sourceFor($event))
            ->map(function ($events, string $source) use ($ordersBySource): array {
                $sessions = $events->pluck('session_id')->filter()->unique()->count();
                $orders = $ordersBySource->get($source, collect())->count();

                return [
                    'source' => $source,
                    'sessions' => $sessions,
                    'orders' => $orders,
                    'conversion_rate' => $sessions > 0 ? round(($orders / $sessions) * 100, 2) : 0.0,
                ];
            })
            ->sortByDesc('sessions')
            ->take(5)
            ->values()
            ->all();
    }

    private function filteredEvents(CarbonInterface $start, CarbonInterface $end, AnalyticsEventType $type): Collection
    {
        return AnalyticsEvent::withoutGlobalScopes()
            ->where('store_id', $this->storeId)
            ->where('type', $type->value)
            ->whereBetween('occurred_at', [$start, $end])
            ->get()
            ->filter(fn (AnalyticsEvent $event): bool => $this->eventMatchesFilters($event))
            ->values();
    }

    private function eventMatchesFilters(AnalyticsEvent $event): bool
    {
        if ($this->channelFilter !== 'all' && data_get($event->properties_json, 'channel') !== $this->channelFilter) {
            return false;
        }

        return $this->deviceFilter === 'all'
            || data_get($event->properties_json, 'device') === $this->deviceFilter;
    }

    private function sourceFor(AnalyticsEvent $event): string
    {
        $referrer = (string) data_get($event->properties_json, 'referrer', 'direct');

        if ($referrer === '' || $referrer === 'direct') {
            return 'Direct';
        }

        $host = parse_url($referrer, PHP_URL_HOST);

        return $host ? str_replace('www.', '', $host) : 'Direct';
    }

    /**
     * @return array{0: CarbonInterface, 1: CarbonInterface}
     */
    private function currentRange(): array
    {
        return match ($this->dateRange) {
            'today' => [now()->startOfDay(), now()->endOfDay()],
            'last_7_days' => [now()->subDays(6)->startOfDay(), now()->endOfDay()],
            'custom' => $this->customRange() ?? [now()->subDays(29)->startOfDay(), now()->endOfDay()],
            default => [now()->subDays(29)->startOfDay(), now()->endOfDay()],
        };
    }


server: laravel-boost

tool: database-schema

filter: analytics

type: text

engine: sqlite

store_id: integer

date: varchar

orders_count: integer

revenue_amount: integer

aov_amount: integer

visits_count: integer

add_to_cart_count: integer

checkout_started_count: integer

checkout_completed_count: integer

type: varchar

session_id: varchar

customer_id: integer

properties_json: text

client_event_id: varchar

occurred_at: datetime

created_at: datetime


cmd: sed -n '280,560p' app/Livewire/Admin/Analytics/Index.php


command: sed -n '280,560p' app/Livewire/Admin/Analytics/Index.php

Index.php

type: read

aggregated_output:
    }

    /**
     * @return array{0: CarbonInterface, 1: CarbonInterface}|null
     */
    private function customRange(): ?array
    {
        if (! $this->customStartDate || ! $this->customEndDate) {
            return null;
        }

        try {
            $start = Carbon::parse($this->customStartDate)->startOfDay();
            $end = Carbon::parse($this->customEndDate)->endOfDay();
        } catch (Throwable) {
            return null;
        }

        if ($end->lessThan($start)) {
            return [$end->startOfDay(), $start->endOfDay()];
        }

        return [$start, $end];
    }

    private function store(): Store
    {
        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        return $store;
    }

    private function scopedStore(): Store
    {
        return Store::query()->findOrFail($this->storeId);
    }

    private function canViewAnalytics(Store $store): bool
    {
        $role = auth()->user()?->roleForStoreId($store->getKey());

        return in_array($role, [StoreUserRole::Owner, StoreUserRole::Admin, StoreUserRole::Staff], true);
    }
}

status: completed


output:
Chunk ID: e9f00b
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 284
Output:
    }

    /**
     * @return array{0: CarbonInterface, 1: CarbonInterface}|null
     */
    private function customRange(): ?array
    {
        if (! $this->customStartDate || ! $this->customEndDate) {
            return null;
        }

        try {
            $start = Carbon::parse($this->customStartDate)->startOfDay();
            $end = Carbon::parse($this->customEndDate)->endOfDay();
        } catch (Throwable) {
            return null;
        }

        if ($end->lessThan($start)) {
            return [$end->startOfDay(), $start->endOfDay()];
        }

        return [$start, $end];
    }

    private function store(): Store
    {
        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        return $store;
    }

    private function scopedStore(): Store
    {
        return Store::query()->findOrFail($this->storeId);
    }

    private function canViewAnalytics(Store $store): bool
    {
        $role = auth()->user()?->roleForStoreId($store->getKey());

        return in_array($role, [StoreUserRole::Owner, StoreUserRole::Admin, StoreUserRole::Staff], true);
    }
}


cmd: sed -n '1,220p' app/Models/AnalyticsDaily.php


cmd: sed -n '1,220p' database/factories/AnalyticsDailyFactory.php


cmd: sed -n '1,220p' database/seeders/AnalyticsSeeder.php


cmd: rg -n "Order::factory|OrderLine::factory|FinancialStatus::Paid|placed_at" tests database/factories app -g '*.php'


command: sed -n '1,220p' app/Models/AnalyticsDaily.php

aggregated_output:
<?php

namespace App\Models;

use App\Models\Concerns\BelongsToStore;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class AnalyticsDaily extends Model
{
    /** @use HasFactory<\Database\Factories\AnalyticsDailyFactory> */
    use BelongsToStore, HasFactory;

    public $incrementing = false;

    public $timestamps = false;

    protected $table = 'analytics_daily';

    protected $primaryKey = 'store_id';

    /**
     * @var list<string>
     */
    protected $fillable = [
        'store_id',
        'date',
        'orders_count',
        'revenue_amount',
        'aov_amount',
        'visits_count',
        'add_to_cart_count',
        'checkout_started_count',
        'checkout_completed_count',
    ];

    /**
     * @return BelongsTo<Store, $this>
     */
    public function store(): BelongsTo
    {
        return $this->belongsTo(Store::class);
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'orders_count' => 'integer',
            'revenue_amount' => 'integer',
            'aov_amount' => 'integer',
            'visits_count' => 'integer',
            'add_to_cart_count' => 'integer',
            'checkout_started_count' => 'integer',
            'checkout_completed_count' => 'integer',
        ];
    }
}


command: sed -n '1,220p' database/factories/AnalyticsDailyFactory.php

aggregated_output:
<?php

namespace Database\Factories;

use App\Models\Store;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\AnalyticsDaily>
 */
class AnalyticsDailyFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        $orders = fake()->numberBetween(0, 12);
        $revenue = $orders * fake()->numberBetween(2500, 9500);

        return [
            'store_id' => Store::factory(),
            'date' => now()->toDateString(),
            'orders_count' => $orders,
            'revenue_amount' => $revenue,
            'aov_amount' => $orders > 0 ? intdiv($revenue, $orders) : 0,
            'visits_count' => fake()->numberBetween(20, 200),
            'add_to_cart_count' => fake()->numberBetween(0, 40),
            'checkout_started_count' => fake()->numberBetween(0, 20),
            'checkout_completed_count' => $orders,
        ];
    }
}


command: sed -n '1,220p' database/seeders/AnalyticsSeeder.php

aggregated_output:
<?php

namespace Database\Seeders;

use App\Enums\AnalyticsEventType;
use App\Models\AnalyticsEvent;
use App\Models\Customer;
use App\Models\Store;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class AnalyticsSeeder extends Seeder
{
    public function run(): void
    {
        Store::query()->get()->each(function (Store $store): void {
            $this->seedDailyMetrics($store);

            if ($store->handle === 'acme-fashion') {
                $this->seedEventStream($store);
            }
        });
    }

    private function seedDailyMetrics(Store $store): void
    {
        foreach (range(0, 29) as $daysAgo) {
            $date = now()->subDays($daysAgo)->toDateString();
            $orders = max(0, 10 - ($daysAgo % 6));
            $revenue = $orders * (5200 + (($daysAgo % 5) * 800));

            DB::table('analytics_daily')->updateOrInsert(
                [
                    'store_id' => $store->getKey(),
                    'date' => $date,
                ],
                [
                    'orders_count' => $orders,
                    'revenue_amount' => $revenue,
                    'aov_amount' => $orders > 0 ? intdiv($revenue, $orders) : 0,
                    'visits_count' => 160 + (($daysAgo * 7) % 80),
                    'add_to_cart_count' => 42 + ($daysAgo % 13),
                    'checkout_started_count' => 18 + ($daysAgo % 8),
                    'checkout_completed_count' => $orders,
                ],
            );
        }
    }

    private function seedEventStream(Store $store): void
    {
        $customer = Customer::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->first();

        $types = [
            AnalyticsEventType::PageView,
            AnalyticsEventType::PageView,
            AnalyticsEventType::PageView,
            AnalyticsEventType::ProductView,
            AnalyticsEventType::ProductView,
            AnalyticsEventType::AddToCart,
            AnalyticsEventType::CheckoutStarted,
            AnalyticsEventType::CheckoutCompleted,
            AnalyticsEventType::Search,
            AnalyticsEventType::RemoveFromCart,
        ];

        $referrers = [
            'https://google.com/search?q=acme+fashion',
            'https://instagram.com/acme-fashion',
            'direct',
            'https://newsletter.example/acme',
        ];

        foreach (range(1, 210) as $index) {
            $type = $types[$index % count($types)];
            $occurredAt = now()
                ->subDays($index % 7)
                ->setTime($index % 24, ($index * 7) % 60, 0);

            AnalyticsEvent::withoutGlobalScopes()->updateOrCreate(
                [
                    'store_id' => $store->getKey(),
                    'client_event_id' => "seed_evt_{$store->handle}_{$index}",
                ],
                [
                    'type' => $type,
                    'session_id' => 'seed_sess_'.($index % 48),
                    'customer_id' => $index % 5 === 0 ? $customer?->getKey() : null,
                    'properties_json' => $this->propertiesFor($type, $index, $referrers[$index % count($referrers)]),
                    'occurred_at' => $occurredAt,
                    'created_at' => $occurredAt,
                ],
            );
        }
    }

    /**
     * @return array<string, mixed>
     */
    private function propertiesFor(AnalyticsEventType $type, int $index, string $referrer): array
    {
        $base = [
            'channel' => $index % 4 === 0 ? 'api' : 'storefront',
            'device' => ['desktop', 'mobile', 'tablet'][$index % 3],
            'referrer' => $referrer,
        ];

        return match ($type) {
            AnalyticsEventType::PageView => [
                ...$base,
                'url' => $index % 2 === 0 ? '/' : '/collections/t-shirts',
            ],
            AnalyticsEventType::ProductView => [
                ...$base,
                'product_id' => ($index % 20) + 1,
                'product_title' => 'Seeded product '.$index,
                'url' => '/products/classic-cotton-t-shirt',
            ],
            AnalyticsEventType::AddToCart, AnalyticsEventType::RemoveFromCart => [
                ...$base,
                'product_id' => ($index % 20) + 1,
                'variant_id' => ($index % 120) + 1,
                'quantity' => 1 + ($index % 3),
                'price_amount' => 2499 + (($index % 5) * 500),
            ],
            AnalyticsEventType::CheckoutStarted => [
                ...$base,
                'cart_id' => $index,
                'subtotal_amount' => 4500 + (($index % 9) * 500),
            ],
            AnalyticsEventType::CheckoutCompleted => [
                ...$base,
                'order_id' => $index,
                'order_number' => 'SEED-'.$index,
                'total_amount' => 6500 + (($index % 9) * 750),
            ],
            AnalyticsEventType::Search => [
                ...$base,
                'query' => ['cotton', 'hoodie', 'jeans', 'sneakers'][$index % 4],
                'results_count' => 3 + ($index % 8),
            ],
        };
    }
}


command: rg -n "Order::factory|OrderLine::factory|FinancialStatus::Paid|placed_at" tests database/factories app -g '*.php'

type: search

cmd: rg -n 'Order::factory|OrderLine::factory|FinancialStatus::Paid|placed_at' tests database/factories app -g '*.php'

query: Order::factory|OrderLine::factory|FinancialStatus::Paid|placed_at

path: tests

aggregated_output:
app/Listeners/DispatchWebhooks.php:103:                'placed_at' => $order->placed_at?->toISOString(),
database/factories/FulfillmentLineFactory.php:23:            'order_line_id' => OrderLine::factory(),
app/Services/FulfillmentService.php:187:        if (! in_array($order->financial_status, [FinancialStatus::Paid, FinancialStatus::PartiallyRefunded], true)) {
database/factories/OrderFactory.php:37:            'financial_status' => FinancialStatus::Paid,
database/factories/OrderFactory.php:48:            'placed_at' => now(),
database/factories/OrderFactory.php:70:            'financial_status' => FinancialStatus::Paid,
database/factories/OrderFactory.php:124:            'financial_status' => FinancialStatus::Paid,
app/Http/Controllers/Api/Admin/V1/CustomerController.php:72:            'orders' => fn ($query) => $query->latest('placed_at')->limit(10),
database/factories/FulfillmentFactory.php:22:            'order_id' => Order::factory(),
tests/Feature/Orders/OrderServiceTest.php:163:        ->and($order->financial_status)->toBe(FinancialStatus::Paid)
database/factories/RefundFactory.php:23:            'order_id' => Order::factory(),
tests/Feature/Orders/BankTransferOrderTest.php:53:    $order = Order::factory()->bankTransfer()->create([
tests/Feature/Orders/BankTransferOrderTest.php:58:        'placed_at' => now()->subDays($ageDays),
tests/Feature/Orders/BankTransferOrderTest.php:64:    OrderLine::factory()->create([
tests/Feature/Orders/BankTransferOrderTest.php:89:        ->and($paidOrder->financial_status)->toBe(FinancialStatus::Paid)
tests/Feature/Admin/CustomerManagementTest.php:45:    return Order::factory()->paid()->create([
tests/Feature/Orders/RefundServiceTest.php:45:    $order = Order::factory()->paid()->create([
tests/Feature/Orders/RefundServiceTest.php:52:    $line = OrderLine::factory()->create([
tests/Feature/Admin/OrderManagementTest.php:64:    $order = Order::factory()->paid()->create(array_merge([
tests/Feature/Admin/OrderManagementTest.php:72:    $line = OrderLine::factory()->create([
tests/Feature/Admin/OrderManagementTest.php:173:        ->and($order->financial_status)->toBe(FinancialStatus::Paid)
tests/Feature/Orders/FulfillmentServiceTest.php:25:function fulfillmentServiceOrder(FinancialStatus $financialStatus = FinancialStatus::Paid): array
tests/Feature/Orders/FulfillmentServiceTest.php:35:    $order = Order::factory()->paid()->create([
tests/Feature/Orders/FulfillmentServiceTest.php:40:    $firstLine = OrderLine::factory()->create([
tests/Feature/Orders/FulfillmentServiceTest.php:48:    $secondLine = OrderLine::factory()->create([
app/Services/OrderService.php:86:                    'financial_status' => $paidImmediately ? FinancialStatus::Paid : FinancialStatus::Pending,
app/Services/OrderService.php:97:                    'placed_at' => now(),
app/Services/OrderService.php:162:                'financial_status' => FinancialStatus::Paid,
database/factories/PaymentFactory.php:23:            'order_id' => Order::factory(),
app/Http/Controllers/Api/Admin/V1/OrderController.php:45:            ->latest('placed_at')
app/Jobs/CancelUnpaidBankTransferOrders.php:25:            ->whereNotNull('placed_at')
app/Jobs/CancelUnpaidBankTransferOrders.php:30:                if ($order->placed_at->lessThanOrEqualTo(now()->subDays($cancelDays))) {
database/factories/OrderLineFactory.php:25:            'order_id' => Order::factory(),
tests/Feature/Admin/DashboardTest.php:46:    $order = Order::factory()->paid()->create(array_merge([
tests/Feature/Admin/DashboardTest.php:52:        'placed_at' => now(),
tests/Feature/Admin/DashboardTest.php:55:    OrderLine::factory()->create([
tests/Feature/Admin/DashboardTest.php:95:        'placed_at' => now()->subDays(15),
tests/Feature/Api/AdminCatalogApiTest.php:89:    Order::factory()->paid()->forCustomer($customer)->create([
tests/Feature/Storefront/OrderViewsTest.php:130:    $order = Order::factory()->forCustomer($customer)->paid()->create([
tests/Feature/Storefront/OrderViewsTest.php:161:    $order = Order::factory()->forCustomer($customer)->paid()->create([
tests/Feature/Storefront/OrderViewsTest.php:166:    $otherOrder = Order::factory()->forCustomer($otherCustomer)->paid()->create([
tests/Feature/Storefront/OrderViewsTest.php:171:    OrderLine::factory()->create([
tests/Feature/Api/AdminOrderApiTest.php:65:    $order = Order::factory()->paid()->create(array_merge([
tests/Feature/Api/AdminOrderApiTest.php:73:    $line = OrderLine::factory()->create([
tests/Feature/Checkout/PricingServicesTest.php:219:    $order = Order::factory()
tests/Feature/Checkout/PricingServicesTest.php:225:    OrderLine::factory()->create([
app/Models/Order.php:41:        'placed_at',
app/Models/Order.php:132:            'placed_at' => 'datetime',
app/Http/Resources/Admin/V1/CustomerResource.php:39:                'placed_at' => $order->placed_at?->toIso8601String(),
app/Http/Resources/Storefront/V1/OrderResource.php:69:            'placed_at' => $this->placed_at?->toIso8601String(),
app/Http/Resources/Admin/V1/OrderResource.php:61:            'placed_at' => $this->placed_at?->toIso8601String(),
app/Livewire/Admin/Analytics/Index.php:188:            ->whereBetween('orders.placed_at', [$start, $end])
app/Livewire/Admin/Analytics/Index.php:190:                FinancialStatus::Paid->value,
app/Livewire/Admin/Customers/Show.php:138:            ->latest('placed_at')
app/Livewire/Admin/Dashboard.php:136:            ->selectRaw('date(placed_at) as order_date, count(*) as aggregate')
app/Livewire/Admin/Dashboard.php:160:            ->whereBetween('orders.placed_at', [$start, $end])
app/Livewire/Admin/Dashboard.php:265:            ->whereBetween('placed_at', [$start, $end]);
app/Livewire/Admin/Dashboard.php:283:            FinancialStatus::Paid->value,
app/Livewire/Storefront/Account/Orders/Index.php:48:            ->latest('placed_at')
app/Livewire/Admin/Orders/Index.php:98:            ->when($dateFrom instanceof Carbon, fn (Builder $query) => $query->where('placed_at', '>=', $dateFrom))
app/Livewire/Admin/Orders/Index.php:99:            ->when($dateTo instanceof Carbon, fn (Builder $query) => $query->where('placed_at', '<=', $dateTo))
app/Livewire/Admin/Orders/Index.php:100:            ->latest('placed_at')


output:
Chunk ID: 03aed0
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 357
Output:
<?php

namespace App\Models;

use App\Models\Concerns\BelongsToStore;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class AnalyticsDaily extends Model
{
    /** @use HasFactory<\Database\Factories\AnalyticsDailyFactory> */
    use BelongsToStore, HasFactory;

    public $incrementing = false;

    public $timestamps = false;

    protected $table = 'analytics_daily';

    protected $primaryKey = 'store_id';

    /**
     * @var list<string>
     */
    protected $fillable = [
        'store_id',
        'date',
        'orders_count',
        'revenue_amount',
        'aov_amount',
        'visits_count',
        'add_to_cart_count',
        'checkout_started_count',
        'checkout_completed_count',
    ];

    /**
     * @return BelongsTo<Store, $this>
     */
    public function store(): BelongsTo
    {
        return $this->belongsTo(Store::class);
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'orders_count' => 'integer',
            'revenue_amount' => 'integer',
            'aov_amount' => 'integer',
            'visits_count' => 'integer',
            'add_to_cart_count' => 'integer',
            'checkout_started_count' => 'integer',
            'checkout_completed_count' => 'integer',
        ];
    }
}


output:
Chunk ID: 17256b
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 261
Output:
<?php

namespace Database\Factories;

use App\Models\Store;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\AnalyticsDaily>
 */
class AnalyticsDailyFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        $orders = fake()->numberBetween(0, 12);
        $revenue = $orders * fake()->numberBetween(2500, 9500);

        return [
            'store_id' => Store::factory(),
            'date' => now()->toDateString(),
            'orders_count' => $orders,
            'revenue_amount' => $revenue,
            'aov_amount' => $orders > 0 ? intdiv($revenue, $orders) : 0,
            'visits_count' => fake()->numberBetween(20, 200),
            'add_to_cart_count' => fake()->numberBetween(0, 40),
            'checkout_started_count' => fake()->numberBetween(0, 20),
            'checkout_completed_count' => $orders,
        ];
    }
}


output:
Chunk ID: ed9ce2
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1293
Output:
<?php

namespace Database\Seeders;

use App\Enums\AnalyticsEventType;
use App\Models\AnalyticsEvent;
use App\Models\Customer;
use App\Models\Store;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class AnalyticsSeeder extends Seeder
{
    public function run(): void
    {
        Store::query()->get()->each(function (Store $store): void {
            $this->seedDailyMetrics($store);

            if ($store->handle === 'acme-fashion') {
                $this->seedEventStream($store);
            }
        });
    }

    private function seedDailyMetrics(Store $store): void
    {
        foreach (range(0, 29) as $daysAgo) {
            $date = now()->subDays($daysAgo)->toDateString();
            $orders = max(0, 10 - ($daysAgo % 6));
            $revenue = $orders * (5200 + (($daysAgo % 5) * 800));

            DB::table('analytics_daily')->updateOrInsert(
                [
                    'store_id' => $store->getKey(),
                    'date' => $date,
                ],
                [
                    'orders_count' => $orders,
                    'revenue_amount' => $revenue,
                    'aov_amount' => $orders > 0 ? intdiv($revenue, $orders) : 0,
                    'visits_count' => 160 + (($daysAgo * 7) % 80),
                    'add_to_cart_count' => 42 + ($daysAgo % 13),
                    'checkout_started_count' => 18 + ($daysAgo % 8),
                    'checkout_completed_count' => $orders,
                ],
            );
        }
    }

    private function seedEventStream(Store $store): void
    {
        $customer = Customer::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->first();

        $types = [
            AnalyticsEventType::PageView,
            AnalyticsEventType::PageView,
            AnalyticsEventType::PageView,
            AnalyticsEventType::ProductView,
            AnalyticsEventType::ProductView,
            AnalyticsEventType::AddToCart,
            AnalyticsEventType::CheckoutStarted,
            AnalyticsEventType::CheckoutCompleted,
            AnalyticsEventType::Search,
            AnalyticsEventType::RemoveFromCart,
        ];

        $referrers = [
            'https://google.com/search?q=acme+fashion',
            'https://instagram.com/acme-fashion',
            'direct',
            'https://newsletter.example/acme',
        ];

        foreach (range(1, 210) as $index) {
            $type = $types[$index % count($types)];
            $occurredAt = now()
                ->subDays($index % 7)
                ->setTime($index % 24, ($index * 7) % 60, 0);

            AnalyticsEvent::withoutGlobalScopes()->updateOrCreate(
                [
                    'store_id' => $store->getKey(),
                    'client_event_id' => "seed_evt_{$store->handle}_{$index}",
                ],
                [
                    'type' => $type,
                    'session_id' => 'seed_sess_'.($index % 48),
                    'customer_id' => $index % 5 === 0 ? $customer?->getKey() : null,
                    'properties_json' => $this->propertiesFor($type, $index, $referrers[$index % count($referrers)]),
                    'occurred_at' => $occurredAt,
                    'created_at' => $occurredAt,
                ],
            );
        }
    }

    /**
     * @return array<string, mixed>
     */
    private function propertiesFor(AnalyticsEventType $type, int $index, string $referrer): array
    {
        $base = [
            'channel' => $index % 4 === 0 ? 'api' : 'storefront',
            'device' => ['desktop', 'mobile', 'tablet'][$index % 3],
            'referrer' => $referrer,
        ];

        return match ($type) {
            AnalyticsEventType::PageView => [
                ...$base,
                'url' => $index % 2 === 0 ? '/' : '/collections/t-shirts',
            ],
            AnalyticsEventType::ProductView => [
                ...$base,
                'product_id' => ($index % 20) + 1,
                'product_title' => 'Seeded product '.$index,
                'url' => '/products/classic-cotton-t-shirt',
            ],
            AnalyticsEventType::AddToCart, AnalyticsEventType::RemoveFromCart => [
                ...$base,
                'product_id' => ($index % 20) + 1,
                'variant_id' => ($index % 120) + 1,
                'quantity' => 1 + ($index % 3),
                'price_amount' => 2499 + (($index % 5) * 500),
            ],
            AnalyticsEventType::CheckoutStarted => [
                ...$base,
                'cart_id' => $index,
                'subtotal_amount' => 4500 + (($index % 9) * 500),
            ],
            AnalyticsEventType::CheckoutCompleted => [
                ...$base,
                'order_id' => $index,
                'order_number' => 'SEED-'.$index,
                'total_amount' => 6500 + (($index % 9) * 750),
            ],
            AnalyticsEventType::Search => [
                ...$base,
                'query' => ['cotton', 'hoodie', 'jeans', 'sneakers'][$index % 4],
                'results_count' => 3 + ($index % 8),
            ],
        };
    }
}


output:
Chunk ID: bc8577
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1562
Output:
app/Listeners/DispatchWebhooks.php:103:                'placed_at' => $order->placed_at?->toISOString(),
database/factories/FulfillmentLineFactory.php:23:            'order_line_id' => OrderLine::factory(),
app/Services/FulfillmentService.php:187:        if (! in_array($order->financial_status, [FinancialStatus::Paid, FinancialStatus::PartiallyRefunded], true)) {
database/factories/OrderFactory.php:37:            'financial_status' => FinancialStatus::Paid,
database/factories/OrderFactory.php:48:            'placed_at' => now(),
database/factories/OrderFactory.php:70:            'financial_status' => FinancialStatus::Paid,
database/factories/OrderFactory.php:124:            'financial_status' => FinancialStatus::Paid,
app/Http/Controllers/Api/Admin/V1/CustomerController.php:72:            'orders' => fn ($query) => $query->latest('placed_at')->limit(10),
database/factories/FulfillmentFactory.php:22:            'order_id' => Order::factory(),
tests/Feature/Orders/OrderServiceTest.php:163:        ->and($order->financial_status)->toBe(FinancialStatus::Paid)
database/factories/RefundFactory.php:23:            'order_id' => Order::factory(),
tests/Feature/Orders/BankTransferOrderTest.php:53:    $order = Order::factory()->bankTransfer()->create([
tests/Feature/Orders/BankTransferOrderTest.php:58:        'placed_at' => now()->subDays($ageDays),
tests/Feature/Orders/BankTransferOrderTest.php:64:    OrderLine::factory()->create([
tests/Feature/Orders/BankTransferOrderTest.php:89:        ->and($paidOrder->financial_status)->toBe(FinancialStatus::Paid)
tests/Feature/Admin/CustomerManagementTest.php:45:    return Order::factory()->paid()->create([
tests/Feature/Orders/RefundServiceTest.php:45:    $order = Order::factory()->paid()->create([
tests/Feature/Orders/RefundServiceTest.php:52:    $line = OrderLine::factory()->create([
tests/Feature/Admin/OrderManagementTest.php:64:    $order = Order::factory()->paid()->create(array_merge([
tests/Feature/Admin/OrderManagementTest.php:72:    $line = OrderLine::factory()->create([
tests/Feature/Admin/OrderManagementTest.php:173:        ->and($order->financial_status)->toBe(FinancialStatus::Paid)
tests/Feature/Orders/FulfillmentServiceTest.php:25:function fulfillmentServiceOrder(FinancialStatus $financialStatus = FinancialStatus::Paid): array
tests/Feature/Orders/FulfillmentServiceTest.php:35:    $order = Order::factory()->paid()->create([
tests/Feature/Orders/FulfillmentServiceTest.php:40:    $firstLine = OrderLine::factory()->create([
tests/Feature/Orders/FulfillmentServiceTest.php:48:    $secondLine = OrderLine::factory()->create([
app/Services/OrderService.php:86:                    'financial_status' => $paidImmediately ? FinancialStatus::Paid : FinancialStatus::Pending,
app/Services/OrderService.php:97:                    'placed_at' => now(),
app/Services/OrderService.php:162:                'financial_status' => FinancialStatus::Paid,
database/factories/PaymentFactory.php:23:            'order_id' => Order::factory(),
app/Http/Controllers/Api/Admin/V1/OrderController.php:45:            ->latest('placed_at')
app/Jobs/CancelUnpaidBankTransferOrders.php:25:            ->whereNotNull('placed_at')
app/Jobs/CancelUnpaidBankTransferOrders.php:30:                if ($order->placed_at->lessThanOrEqualTo(now()->subDays($cancelDays))) {
database/factories/OrderLineFactory.php:25:            'order_id' => Order::factory(),
tests/Feature/Admin/DashboardTest.php:46:    $order = Order::factory()->paid()->create(array_merge([
tests/Feature/Admin/DashboardTest.php:52:        'placed_at' => now(),
tests/Feature/Admin/DashboardTest.php:55:    OrderLine::factory()->create([
tests/Feature/Admin/DashboardTest.php:95:        'placed_at' => now()->subDays(15),
tests/Feature/Api/AdminCatalogApiTest.php:89:    Order::factory()->paid()->forCustomer($customer)->create([
tests/Feature/Storefront/OrderViewsTest.php:130:    $order = Order::factory()->forCustomer($customer)->paid()->create([
tests/Feature/Storefront/OrderViewsTest.php:161:    $order = Order::factory()->forCustomer($customer)->paid()->create([
tests/Feature/Storefront/OrderViewsTest.php:166:    $otherOrder = Order::factory()->forCustomer($otherCustomer)->paid()->create([
tests/Feature/Storefront/OrderViewsTest.php:171:    OrderLine::factory()->create([
tests/Feature/Api/AdminOrderApiTest.php:65:    $order = Order::factory()->paid()->create(array_merge([
tests/Feature/Api/AdminOrderApiTest.php:73:    $line = OrderLine::factory()->create([
tests/Feature/Checkout/PricingServicesTest.php:219:    $order = Order::factory()
tests/Feature/Checkout/PricingServicesTest.php:225:    OrderLine::factory()->create([
app/Models/Order.php:41:        'placed_at',
app/Models/Order.php:132:            'placed_at' => 'datetime',
app/Http/Resources/Admin/V1/CustomerResource.php:39:                'placed_at' => $order->placed_at?->toIso8601String(),
app/Http/Resources/Storefront/V1/OrderResource.php:69:            'placed_at' => $this->placed_at?->toIso8601String(),
app/Http/Resources/Admin/V1/OrderResource.php:61:            'placed_at' => $this->placed_at?->toIso8601String(),
app/Livewire/Admin/Analytics/Index.php:188:            ->whereBetween('orders.placed_at', [$start, $end])
app/Livewire/Admin/Analytics/Index.php:190:                FinancialStatus::Paid->value,
app/Livewire/Admin/Customers/Show.php:138:            ->latest('placed_at')
app/Livewire/Admin/Dashboard.php:136:            ->selectRaw('date(placed_at) as order_date, count(*) as aggregate')
app/Livewire/Admin/Dashboard.php:160:            ->whereBetween('orders.placed_at', [$start, $end])
app/Livewire/Admin/Dashboard.php:265:            ->whereBetween('placed_at', [$start, $end]);
app/Livewire/Admin/Dashboard.php:283:            FinancialStatus::Paid->value,
app/Livewire/Storefront/Account/Orders/Index.php:48:            ->latest('placed_at')
app/Livewire/Admin/Orders/Index.php:98:            ->when($dateFrom instanceof Carbon, fn (Builder $query) => $query->where('placed_at', '>=', $dateFrom))
app/Livewire/Admin/Orders/Index.php:99:            ->when($dateTo instanceof Carbon, fn (Builder $query) => $query->where('placed_at', '<=', $dateTo))
app/Livewire/Admin/Orders/Index.php:100:            ->latest('placed_at')


cmd: sed -n '1,200p' app/Models/OrderLine.php


cmd: sed -n '1,160p' database/factories/OrderLineFactory.php


cmd: sed -n '1,180p' database/factories/OrderFactory.php


command: sed -n '1,200p' app/Models/OrderLine.php

aggregated_output:
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class OrderLine extends Model
{
    /** @use HasFactory<\Database\Factories\OrderLineFactory> */
    use HasFactory;

    public $timestamps = false;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'order_id',
        'product_id',
        'variant_id',
        'title_snapshot',
        'sku_snapshot',
        'quantity',
        'unit_price_amount',
        'total_amount',
        'tax_lines_json',
        'discount_allocations_json',
    ];

    /**
     * @return BelongsTo<Order, $this>
     */
    public function order(): BelongsTo
    {
        return $this->belongsTo(Order::class);
    }

    /**
     * @return BelongsTo<Product, $this>
     */
    public function product(): BelongsTo
    {
        return $this->belongsTo(Product::class);
    }

    /**
     * @return BelongsTo<ProductVariant, $this>
     */
    public function variant(): BelongsTo
    {
        return $this->belongsTo(ProductVariant::class, 'variant_id');
    }

    /**
     * @return HasMany<FulfillmentLine, $this>
     */
    public function fulfillmentLines(): HasMany
    {
        return $this->hasMany(FulfillmentLine::class);
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'quantity' => 'integer',
            'unit_price_amount' => 'integer',
            'total_amount' => 'integer',
            'tax_lines_json' => 'array',
            'discount_allocations_json' => 'array',
        ];
    }
}


command: sed -n '1,180p' database/factories/OrderFactory.php

aggregated_output:
<?php

namespace Database\Factories;

use App\Enums\FinancialStatus;
use App\Enums\FulfillmentStatus;
use App\Enums\OrderStatus;
use App\Enums\PaymentMethod;
use App\Models\Checkout;
use App\Models\Customer;
use App\Models\Store;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Order>
 */
class OrderFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        $subtotal = fake()->numberBetween(2500, 25000);
        $shipping = fake()->randomElement([0, 499, 799]);
        $tax = 0;

        return [
            'store_id' => Store::factory(),
            'checkout_id' => null,
            'customer_id' => null,
            'order_number' => '#'.fake()->unique()->numberBetween(1001, 9999),
            'payment_method' => PaymentMethod::CreditCard,
            'status' => OrderStatus::Paid,
            'financial_status' => FinancialStatus::Paid,
            'fulfillment_status' => FulfillmentStatus::Unfulfilled,
            'currency' => 'EUR',
            'subtotal_amount' => $subtotal,
            'discount_amount' => 0,
            'shipping_amount' => $shipping,
            'tax_amount' => $tax,
            'total_amount' => $subtotal + $shipping + $tax,
            'email' => fake()->safeEmail(),
            'billing_address_json' => $this->address(),
            'shipping_address_json' => $this->address(),
            'placed_at' => now(),
        ];
    }

    public function forCheckout(?Checkout $checkout = null): static
    {
        return $this->state(fn (array $attributes): array => [
            'checkout_id' => $checkout?->getKey() ?? Checkout::factory(),
        ]);
    }

    public function forCustomer(?Customer $customer = null): static
    {
        return $this->state(fn (array $attributes): array => [
            'customer_id' => $customer?->getKey() ?? Customer::factory(),
        ]);
    }

    public function paid(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => OrderStatus::Paid,
            'financial_status' => FinancialStatus::Paid,
        ]);
    }

    public function pending(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => OrderStatus::Pending,
            'financial_status' => FinancialStatus::Pending,
        ]);
    }

    public function creditCard(): static
    {
        return $this->state(fn (array $attributes): array => [
            'payment_method' => PaymentMethod::CreditCard,
        ]);
    }

    public function paypal(): static
    {
        return $this->state(fn (array $attributes): array => [
            'payment_method' => PaymentMethod::Paypal,
        ]);
    }

    public function bankTransfer(): static
    {
        return $this->state(fn (array $attributes): array => [
            'payment_method' => PaymentMethod::BankTransfer,
            'status' => OrderStatus::Pending,
            'financial_status' => FinancialStatus::Pending,
        ]);
    }

    public function cancelled(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => OrderStatus::Cancelled,
            'financial_status' => FinancialStatus::Voided,
        ]);
    }

    public function partiallyFulfilled(): static
    {
        return $this->state(fn (array $attributes): array => [
            'fulfillment_status' => FulfillmentStatus::Partial,
        ]);
    }

    public function fulfilled(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => OrderStatus::Fulfilled,
            'financial_status' => FinancialStatus::Paid,
            'fulfillment_status' => FulfillmentStatus::Fulfilled,
        ]);
    }

    public function refunded(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => OrderStatus::Refunded,
            'financial_status' => FinancialStatus::Refunded,
        ]);
    }

    /**
     * @return array<string, string|null>
     */
    private function address(): array
    {
        return [
            'first_name' => fake()->firstName(),
            'last_name' => fake()->lastName(),
            'address1' => fake()->streetAddress(),
            'address2' => null,
            'city' => fake()->city(),
            'province_code' => null,
            'country' => 'DE',
            'postal_code' => fake()->postcode(),
        ];
    }
}


command: sed -n '1,160p' database/factories/OrderLineFactory.php

aggregated_output:
<?php

namespace Database\Factories;

use App\Models\Order;
use App\Models\ProductVariant;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\OrderLine>
 */
class OrderLineFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        $quantity = fake()->numberBetween(1, 3);
        $unitPrice = fake()->numberBetween(1000, 10000);

        return [
            'order_id' => Order::factory(),
            'product_id' => null,
            'variant_id' => ProductVariant::factory(),
            'title_snapshot' => fake()->words(3, true),
            'sku_snapshot' => 'SKU-'.fake()->unique()->numerify('####'),
            'quantity' => $quantity,
            'unit_price_amount' => $unitPrice,
            'total_amount' => $unitPrice * $quantity,
            'tax_lines_json' => [],
            'discount_allocations_json' => [],
        ];
    }
}


output:
Chunk ID: 48597e
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 436
Output:
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class OrderLine extends Model
{
    /** @use HasFactory<\Database\Factories\OrderLineFactory> */
    use HasFactory;

    public $timestamps = false;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'order_id',
        'product_id',
        'variant_id',
        'title_snapshot',
        'sku_snapshot',
        'quantity',
        'unit_price_amount',
        'total_amount',
        'tax_lines_json',
        'discount_allocations_json',
    ];

    /**
     * @return BelongsTo<Order, $this>
     */
    public function order(): BelongsTo
    {
        return $this->belongsTo(Order::class);
    }

    /**
     * @return BelongsTo<Product, $this>
     */
    public function product(): BelongsTo
    {
        return $this->belongsTo(Product::class);
    }

    /**
     * @return BelongsTo<ProductVariant, $this>
     */
    public function variant(): BelongsTo
    {
        return $this->belongsTo(ProductVariant::class, 'variant_id');
    }

    /**
     * @return HasMany<FulfillmentLine, $this>
     */
    public function fulfillmentLines(): HasMany
    {
        return $this->hasMany(FulfillmentLine::class);
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'quantity' => 'integer',
            'unit_price_amount' => 'integer',
            'total_amount' => 'integer',
            'tax_lines_json' => 'array',
            'discount_allocations_json' => 'array',
        ];
    }
}


output:
Chunk ID: c8b94d
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 262
Output:
<?php

namespace Database\Factories;

use App\Models\Order;
use App\Models\ProductVariant;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\OrderLine>
 */
class OrderLineFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        $quantity = fake()->numberBetween(1, 3);
        $unitPrice = fake()->numberBetween(1000, 10000);

        return [
            'order_id' => Order::factory(),
            'product_id' => null,
            'variant_id' => ProductVariant::factory(),
            'title_snapshot' => fake()->words(3, true),
            'sku_snapshot' => 'SKU-'.fake()->unique()->numerify('####'),
            'quantity' => $quantity,
            'unit_price_amount' => $unitPrice,
            'total_amount' => $unitPrice * $quantity,
            'tax_lines_json' => [],
            'discount_allocations_json' => [],
        ];
    }
}


output:
Chunk ID: ead9b0
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1152
Output:
<?php

namespace Database\Factories;

use App\Enums\FinancialStatus;
use App\Enums\FulfillmentStatus;
use App\Enums\OrderStatus;
use App\Enums\PaymentMethod;
use App\Models\Checkout;
use App\Models\Customer;
use App\Models\Store;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Order>
 */
class OrderFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        $subtotal = fake()->numberBetween(2500, 25000);
        $shipping = fake()->randomElement([0, 499, 799]);
        $tax = 0;

        return [
            'store_id' => Store::factory(),
            'checkout_id' => null,
            'customer_id' => null,
            'order_number' => '#'.fake()->unique()->numberBetween(1001, 9999),
            'payment_method' => PaymentMethod::CreditCard,
            'status' => OrderStatus::Paid,
            'financial_status' => FinancialStatus::Paid,
            'fulfillment_status' => FulfillmentStatus::Unfulfilled,
            'currency' => 'EUR',
            'subtotal_amount' => $subtotal,
            'discount_amount' => 0,
            'shipping_amount' => $shipping,
            'tax_amount' => $tax,
            'total_amount' => $subtotal + $shipping + $tax,
            'email' => fake()->safeEmail(),
            'billing_address_json' => $this->address(),
            'shipping_address_json' => $this->address(),
            'placed_at' => now(),
        ];
    }

    public function forCheckout(?Checkout $checkout = null): static
    {
        return $this->state(fn (array $attributes): array => [
            'checkout_id' => $checkout?->getKey() ?? Checkout::factory(),
        ]);
    }

    public function forCustomer(?Customer $customer = null): static
    {
        return $this->state(fn (array $attributes): array => [
            'customer_id' => $customer?->getKey() ?? Customer::factory(),
        ]);
    }

    public function paid(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => OrderStatus::Paid,
            'financial_status' => FinancialStatus::Paid,
        ]);
    }

    public function pending(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => OrderStatus::Pending,
            'financial_status' => FinancialStatus::Pending,
        ]);
    }

    public function creditCard(): static
    {
        return $this->state(fn (array $attributes): array => [
            'payment_method' => PaymentMethod::CreditCard,
        ]);
    }

    public function paypal(): static
    {
        return $this->state(fn (array $attributes): array => [
            'payment_method' => PaymentMethod::Paypal,
        ]);
    }

    public function bankTransfer(): static
    {
        return $this->state(fn (array $attributes): array => [
            'payment_method' => PaymentMethod::BankTransfer,
            'status' => OrderStatus::Pending,
            'financial_status' => FinancialStatus::Pending,
        ]);
    }

    public function cancelled(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => OrderStatus::Cancelled,
            'financial_status' => FinancialStatus::Voided,
        ]);
    }

    public function partiallyFulfilled(): static
    {
        return $this->state(fn (array $attributes): array => [
            'fulfillment_status' => FulfillmentStatus::Partial,
        ]);
    }

    public function fulfilled(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => OrderStatus::Fulfilled,
            'financial_status' => FinancialStatus::Paid,
            'fulfillment_status' => FulfillmentStatus::Fulfilled,
        ]);
    }

    public function refunded(): static
    {
        return $this->state(fn (array $attributes): array => [
            'status' => OrderStatus::Refunded,
            'financial_status' => FinancialStatus::Refunded,
        ]);
    }

    /**
     * @return array<string, string|null>
     */
    private function address(): array
    {
        return [
            'first_name' => fake()->firstName(),
            'last_name' => fake()->lastName(),
            'address1' => fake()->streetAddress(),
            'address2' => null,
            'city' => fake()->city(),
            'province_code' => null,
            'country' => 'DE',
            'postal_code' => fake()->postcode(),
        ];
    }
}


message: The analytics summary endpoint needs date-range validation, totals from `analytics_daily`, and top products from paid order lines. I’m creating a controller/test pair and keeping the response shape aligned with the spec’s `summary`, `daily`, and `top_products` blocks.

phase: commentary


type: message

role: assistant

text: The analytics summary endpoint needs date-range validation, totals from `analytics_daily`, and top products from paid order lines. I’m creating a controller/test pair and keeping the response shape aligned with the spec’s `summary`, `daily`, and `top_products` blocks.


cmd: php artisan make:controller Api/Admin/V1/AnalyticsSummaryController --no-interaction


cmd: php artisan make:test Api/AdminAnalyticsSummaryApiTest --pest --no-interaction


output:
Chunk ID: 9c02b5
Wall time: 0.1435 seconds
Process exited with code 0
Original token count: 29
Output:

   INFO  Controller [app/Http/Controllers/Api/Admin/V1/AnalyticsSummaryController.php] created successfully.


output:
Chunk ID: 7d57f3
Wall time: 0.1449 seconds
Process exited with code 0
Original token count: 24
Output:

   INFO  Test [tests/Feature/Api/AdminAnalyticsSummaryApiTest.php] created successfully.


command: php artisan make:controller Api/Admin/V1/AnalyticsSummaryController --no-interaction

type: unknown

aggregated_output: INFO  Controller [app/Http/Controllers/Api/Admin/V1/AnalyticsSummaryController.php] created successfully.


command: php artisan make:test Api/AdminAnalyticsSummaryApiTest --pest --no-interaction

aggregated_output: INFO  Test [tests/Feature/Api/AdminAnalyticsSummaryApiTest.php] created successfully.


cmd: sed -n '1,120p' database/migrations/*analytics_daily*


command: sed -n '1,120p' database/migrations/*analytics_daily*

cmd: sed -n '1,120p' 'database/migrations/*analytics_daily*'

aggregated_output: zsh:1: no matches found: database/migrations/*analytics_daily*

status: failed


output:
Chunk ID: 3df54e
Wall time: 0.0000 seconds
Process exited with code 1
Original token count: 16
Output:
zsh:1: no matches found: database/migrations/*analytics_daily*


cmd: rg -n "analytics_daily" database/migrations


command: rg -n "analytics_daily" database/migrations

cmd: rg -n analytics_daily database/migrations

path: migrations

aggregated_output:
database/migrations/2026_05_04_032641_create_analytics_dailies_table.php:14:        Schema::create('analytics_daily', function (Blueprint $table) {
database/migrations/2026_05_04_032641_create_analytics_dailies_table.php:26:            $table->index(['store_id', 'date'], 'idx_analytics_daily_store_date');
database/migrations/2026_05_04_032641_create_analytics_dailies_table.php:35:        Schema::dropIfExists('analytics_daily');


output:
Chunk ID: 55943e
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 108
Output:
database/migrations/2026_05_04_032641_create_analytics_dailies_table.php:14:        Schema::create('analytics_daily', function (Blueprint $table) {
database/migrations/2026_05_04_032641_create_analytics_dailies_table.php:26:            $table->index(['store_id', 'date'], 'idx_analytics_daily_store_date');
database/migrations/2026_05_04_032641_create_analytics_dailies_table.php:35:        Schema::dropIfExists('analytics_daily');


cmd: sed -n '1,80p' database/migrations/2026_05_04_032641_create_analytics_dailies_table.php


command: sed -n '1,80p' database/migrations/2026_05_04_032641_create_analytics_dailies_table.php

aggregated_output:
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('analytics_daily', function (Blueprint $table) {
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->string('date');
            $table->unsignedInteger('orders_count')->default(0);
            $table->unsignedBigInteger('revenue_amount')->default(0);
            $table->unsignedInteger('aov_amount')->default(0);
            $table->unsignedInteger('visits_count')->default(0);
            $table->unsignedInteger('add_to_cart_count')->default(0);
            $table->unsignedInteger('checkout_started_count')->default(0);
            $table->unsignedInteger('checkout_completed_count')->default(0);

            $table->primary(['store_id', 'date']);
            $table->index(['store_id', 'date'], 'idx_analytics_daily_store_date');
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('analytics_daily');
    }
};


output:
Chunk ID: 8fc14f
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 305
Output:
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('analytics_daily', function (Blueprint $table) {
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->string('date');
            $table->unsignedInteger('orders_count')->default(0);
            $table->unsignedBigInteger('revenue_amount')->default(0);
            $table->unsignedInteger('aov_amount')->default(0);
            $table->unsignedInteger('visits_count')->default(0);
            $table->unsignedInteger('add_to_cart_count')->default(0);
            $table->unsignedInteger('checkout_started_count')->default(0);
            $table->unsignedInteger('checkout_completed_count')->default(0);

            $table->primary(['store_id', 'date']);
            $table->index(['store_id', 'date'], 'idx_analytics_daily_store_date');
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('analytics_daily');
    }
};


status: completed

input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/app/Http/Controllers/Api/Admin/V1/AnalyticsSummaryController.php
@@
 <?php

 namespace App\Http\Controllers\Api\Admin\V1;

+use App\Enums\FinancialStatus;
 use App\Http\Controllers\Controller;
+use App\Models\OrderLine;
+use App\Models\Store;
+use App\Services\AnalyticsService;
+use Carbon\CarbonInterface;
+use Carbon\CarbonPeriod;
+use Illuminate\Http\JsonResponse;
 use Illuminate\Http\Request;
+use Illuminate\Support\Carbon;
+use Illuminate\Support\Collection;
+use Illuminate\Validation\Rule;
+use Illuminate\Validation\ValidationException;

 class AnalyticsSummaryController extends Controller
 {
-    //
+    public function show(Request $request, Store $store, AnalyticsService $analytics): JsonResponse
+    {
+        $this->authorizeStore($request, $store);
+
+        $validated = $request->validate([
+            'from' => ['required', 'date_format:Y-m-d'],
+            'to' => ['required', 'date_format:Y-m-d', 'after_or_equal:from'],
+            'granularity' => ['nullable', Rule::in(['day', 'week', 'month'])],
+        ]);
+
+        $from = Carbon::createFromFormat('Y-m-d', (string) $validated['from'])->startOfDay();
+        $to = Carbon::createFromFormat('Y-m-d', (string) $validated['to'])->endOfDay();
+
+        if ($from->diffInDays($to) > 365) {
+            throw ValidationException::withMessages([
+                'to' => __('The selected date range may not be greater than 365 days.'),
+            ]);
+        }
+
+        $totals = $analytics->totals($store, $from->toDateString(), $to->toDateString());
+
+        return response()->json([
+            'data' => [
+                'period' => [
+                    'from' => $from->toDateString(),
+                    'to' => $to->toDateString(),
+                ],
+                'summary' => [
+                    'orders_count' => $totals['orders_count'],
+                    'revenue_amount' => $totals['revenue_amount'],
+                    'aov_amount' => $totals['aov_amount'],
+                    'visits_count' => $totals['visits_count'],
+                    'add_to_cart_count' => $totals['add_to_cart_count'],
+                    'checkout_started_count' => $totals['checkout_started_count'],
+                    'conversion_rate' => $totals['visits_count'] > 0 ? round($totals['checkout_completed_count'] / $totals['visits_count'], 4) : 0.0,
+                    'currency' => $store->default_currency,
+                ],
+                'daily' => $this->metricRows($analytics, $store, $from, $to, $validated['granularity'] ?? 'day'),
+                'top_products' => $this->topProducts($store, $from, $to),
+            ],
+        ]);
+    }
+
+    private function authorizeStore(Request $request, Store $store): void
+    {
+        if (! $request->attributes->has('admin_api_oauth_token')) {
+            abort_unless($request->user()?->stores()->whereKey($store->getKey())->exists(), 403);
+        }
+
+        app()->instance('current_store', $store);
+    }
+
+    /**
+     * @return list<array{date: string, orders_count: int, revenue_amount: int, aov_amount: int, visits_count: int, add_to_cart_count: int, checkout_started_count: int}>
+     */
+    private function metricRows(AnalyticsService $analytics, Store $store, CarbonInterface $from, CarbonInterface $to, string $granularity): array
+    {
+        $metrics = $analytics->getDailyMetrics($store, $from->toDateString(), $to->toDateString());
+
+        if ($granularity === 'day') {
+            $metricsByDate = $metrics->keyBy('date');
+
+            return collect(CarbonPeriod::create($from->copy()->startOfDay(), '1 day', $to->copy()->startOfDay()))
+                ->map(fn (CarbonInterface $date): array => $this->metricRow($date->toDateString(), collect([$metricsByDate->get($date->toDateString())])->filter()))
+                ->values()
+                ->all();
+        }
+
+        return $metrics
+            ->groupBy(fn ($metric): string => $this->periodStart((string) $metric->date, $granularity))
+            ->map(fn (Collection $rows, string $date): array => $this->metricRow($date, $rows))
+            ->values()
+            ->all();
+    }
+
+    /**
+     * @param  Collection<int, mixed>  $metrics
+     * @return array{date: string, orders_count: int, revenue_amount: int, aov_amount: int, visits_count: int, add_to_cart_count: int, checkout_started_count: int}
+     */
+    private function metricRow(string $date, Collection $metrics): array
+    {
+        $orders = (int) $metrics->sum('orders_count');
+        $revenue = (int) $metrics->sum('revenue_amount');
+
+        return [
+            'date' => $date,
+            'orders_count' => $orders,
+            'revenue_amount' => $revenue,
+            'aov_amount' => $orders > 0 ? intdiv($revenue, $orders) : 0,
+            'visits_count' => (int) $metrics->sum('visits_count'),
+            'add_to_cart_count' => (int) $metrics->sum('add_to_cart_count'),
+            'checkout_started_count' => (int) $metrics->sum('checkout_started_count'),
+        ];
+    }
+
+    private function periodStart(string $date, string $granularity): string
+    {
+        $date = Carbon::parse($date);
+
+        return match ($granularity) {
+            'week' => $date->startOfWeek()->toDateString(),
+            'month' => $date->startOfMonth()->toDateString(),
+            default => $date->toDateString(),
+        };
+    }
+
+    /**
+     * @return list<array{product_id: int|null, title: string, units_sold: int, revenue_amount: int}>
+     */
+    private function topProducts(Store $store, CarbonInterface $from, CarbonInterface $to): array
+    {
+        return OrderLine::query()
+            ->selectRaw('order_lines.product_id, order_lines.title_snapshot as title, sum(order_lines.quantity) as units_sold, sum(order_lines.total_amount) as revenue_amount')
+            ->join('orders', 'orders.id', '=', 'order_lines.order_id')
+            ->where('orders.store_id', $store->getKey())
+            ->whereBetween('orders.placed_at', [$from, $to])
+            ->whereIn('orders.financial_status', [
+                FinancialStatus::Paid->value,
+                FinancialStatus::PartiallyRefunded->value,
+            ])
+            ->groupBy('order_lines.product_id', 'order_lines.title_snapshot')
+            ->orderByDesc('revenue_amount')
+            ->limit(10)
+            ->get()
+            ->map(fn (OrderLine $line): array => [
+                'product_id' => $line->product_id === null ? null : (int) $line->product_id,
+                'title' => (string) $line->title,
+                'units_sold' => (int) $line->units_sold,
+                'revenue_amount' => (int) $line->revenue_amount,
+            ])
+            ->all();
+    }
 }
*** Update File: /Users/fabianwesner/Herd/shop/routes/api.php
@@
 use App\Http\Controllers\Api\Admin\V1\CustomerController as AdminCustomerController;
 use App\Http\Controllers\Api\Admin\V1\DiscountController as AdminDiscountController;
+use App\Http\Controllers\Api\Admin\V1\AnalyticsSummaryController as AdminAnalyticsSummaryController;
 use App\Http\Controllers\Api\Admin\V1\OrderController as AdminOrderController;
@@
         Route::middleware('admin.api:write-settings')->group(function (): void {
             Route::post('search/reindex', [AdminSearchIndexController::class, 'reindex'])->name('search.reindex');
         });

+        Route::middleware('admin.api:read-analytics')->group(function (): void {
+            Route::get('analytics/summary', [AdminAnalyticsSummaryController::class, 'show'])->name('analytics.summary');
+        });
+
         Route::middleware('admin.api:read-orders')->group(function (): void {
             Route::get('orders', [AdminOrderController::class, 'index'])->name('orders.index');
             Route::get('orders/{order}', [AdminOrderController::class, 'show'])->name('orders.show');
         });
*** Update File: /Users/fabianwesner/Herd/shop/tests/Feature/Api/AdminAnalyticsSummaryApiTest.php
@@
 <?php

-test('example', function () {
-    $response = $this->get('/');
+use App\Models\Order;
+use App\Models\OrderLine;
+use App\Models\Product;
+use App\Models\Store;
+use App\Models\User;
+use App\Services\WebhookService;
+use Database\Seeders\DatabaseSeeder;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Support\Carbon;
+use Illuminate\Support\Facades\DB;

-    $response->assertStatus(200);
+uses(RefreshDatabase::class);
+
+beforeEach(function (): void {
+    $this->withoutVite();
+    $this->seed(DatabaseSeeder::class);
+});
+
+function adminAnalyticsSummaryApiStore(): Store
+{
+    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
+}
+
+function adminAnalyticsSummaryApiUser(): User
+{
+    return User::query()->where('email', 'admin@acme.test')->firstOrFail();
+}
+
+/**
+ * @param  list<string>  $abilities
+ * @return array{token: \App\Models\OauthToken, plain_text: string}
+ */
+function adminAnalyticsSummaryApiToken(Store $store, array $abilities): array
+{
+    return app(WebhookService::class)->createApiToken($store, 'Analytics integration', $abilities);
+}
+
+test('admin analytics summary api returns totals daily rows and top products', function (): void {
+    $store = adminAnalyticsSummaryApiStore();
+    $from = now()->subDay()->toDateString();
+    $to = now()->toDateString();
+
+    DB::table('analytics_daily')
+        ->where('store_id', $store->getKey())
+        ->whereBetween('date', [$from, $to])
+        ->delete();
+
+    DB::table('analytics_daily')->insert([
+        [
+            'store_id' => $store->getKey(),
+            'date' => $from,
+            'orders_count' => 2,
+            'revenue_amount' => 2000,
+            'aov_amount' => 1000,
+            'visits_count' => 10,
+            'add_to_cart_count' => 4,
+            'checkout_started_count' => 3,
+            'checkout_completed_count' => 2,
+        ],
+        [
+            'store_id' => $store->getKey(),
+            'date' => $to,
+            'orders_count' => 3,
+            'revenue_amount' => 9000,
+            'aov_amount' => 3000,
+            'visits_count' => 20,
+            'add_to_cart_count' => 6,
+            'checkout_started_count' => 7,
+            'checkout_completed_count' => 3,
+        ],
+    ]);
+
+    $product = Product::factory()
+        ->for($store)
+        ->withDefaultVariant(2000)
+        ->create(['title' => 'Analytics API Jacket']);
+    $order = Order::factory()->paid()->create([
+        'store_id' => $store->getKey(),
+        'currency' => $store->default_currency,
+        'placed_at' => Carbon::parse($from)->addHours(12),
+    ]);
+    OrderLine::factory()->create([
+        'order_id' => $order->getKey(),
+        'product_id' => $product->getKey(),
+        'title_snapshot' => $product->title,
+        'quantity' => 4,
+        'total_amount' => 8000,
+    ]);
+
+    $this->actingAs(adminAnalyticsSummaryApiUser())
+        ->getJson("/api/admin/v1/stores/{$store->getKey()}/analytics/summary?from={$from}&to={$to}")
+        ->assertOk()
+        ->assertJsonPath('data.period.from', $from)
+        ->assertJsonPath('data.period.to', $to)
+        ->assertJsonPath('data.summary.orders_count', 5)
+        ->assertJsonPath('data.summary.revenue_amount', 11000)
+        ->assertJsonPath('data.summary.aov_amount', 2200)
+        ->assertJsonPath('data.summary.visits_count', 30)
+        ->assertJsonPath('data.summary.add_to_cart_count', 10)
+        ->assertJsonPath('data.summary.checkout_started_count', 10)
+        ->assertJsonPath('data.summary.conversion_rate', 0.1667)
+        ->assertJsonPath('data.summary.currency', $store->default_currency)
+        ->assertJsonPath('data.daily.0.date', $from)
+        ->assertJsonPath('data.daily.1.date', $to)
+        ->assertJsonPath('data.top_products.0.product_id', $product->getKey())
+        ->assertJsonPath('data.top_products.0.title', 'Analytics API Jacket')
+        ->assertJsonPath('data.top_products.0.units_sold', 4)
+        ->assertJsonPath('data.top_products.0.revenue_amount', 8000);
+});
+
+test('admin analytics summary api enforces token abilities and store scope', function (): void {
+    $store = adminAnalyticsSummaryApiStore();
+    $otherStore = Store::factory()->create();
+    $from = now()->subDay()->toDateString();
+    $to = now()->toDateString();
+    $readToken = adminAnalyticsSummaryApiToken($store, ['read-analytics']);
+    $wrongAbilityToken = adminAnalyticsSummaryApiToken($store, ['read-settings']);
+    $otherStoreToken = adminAnalyticsSummaryApiToken($otherStore, ['read-analytics']);
+
+    $this->withToken($readToken['plain_text'])
+        ->getJson("/api/admin/v1/stores/{$store->getKey()}/analytics/summary?from={$from}&to={$to}")
+        ->assertOk();
+
+    $this->withToken($wrongAbilityToken['plain_text'])
+        ->getJson("/api/admin/v1/stores/{$store->getKey()}/analytics/summary?from={$from}&to={$to}")
+        ->assertForbidden();
+
+    $this->withToken($otherStoreToken['plain_text'])
+        ->getJson("/api/admin/v1/stores/{$store->getKey()}/analytics/summary?from={$from}&to={$to}")
+        ->assertForbidden();
+});
+
+test('admin analytics summary api validates date ranges', function (): void {
+    $store = adminAnalyticsSummaryApiStore();
+
+    $this->actingAs(adminAnalyticsSummaryApiUser())
+        ->getJson("/api/admin/v1/stores/{$store->getKey()}/analytics/summary?from=2026-02-10&to=2026-02-01")
+        ->assertUnprocessable()
+        ->assertJsonValidationErrors(['to']);
+
+    $this->actingAs(adminAnalyticsSummaryApiUser())
+        ->getJson("/api/admin/v1/stores/{$store->getKey()}/analytics/summary?from=2025-01-01&to=2026-02-01")
+        ->assertUnprocessable()
+        ->assertJsonValidationErrors(['to']);
 });
*** End Patch


stdout:
Success. Updated the following files:
M /Users/fabianwesner/Herd/shop/app/Http/Controllers/Api/Admin/V1/AnalyticsSummaryController.php
M /Users/fabianwesner/Herd/shop/routes/api.php
M /Users/fabianwesner/Herd/shop/tests/Feature/Api/AdminAnalyticsSummaryApiTest.php

type: update

unified_diff:
@@ -2,6 +2,146 @@

-test('example', function () {
-    $response = $this->get('/');
+use App\Models\Order;
+use App\Models\OrderLine;
+use App\Models\Product;
+use App\Models\Store;
+use App\Models\User;
+use App\Services\WebhookService;
+use Database\Seeders\DatabaseSeeder;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Support\Carbon;
+use Illuminate\Support\Facades\DB;
+
+uses(RefreshDatabase::class);
+
+beforeEach(function (): void {
+    $this->withoutVite();
+    $this->seed(DatabaseSeeder::class);
+});
+
+function adminAnalyticsSummaryApiStore(): Store
+{
+    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
+}
+
+function adminAnalyticsSummaryApiUser(): User
+{
+    return User::query()->where('email', 'admin@acme.test')->firstOrFail();
+}
+
+/**
+ * @param  list<string>  $abilities
+ * @return array{token: \App\Models\OauthToken, plain_text: string}
+ */
+function adminAnalyticsSummaryApiToken(Store $store, array $abilities): array
+{
+    return app(WebhookService::class)->createApiToken($store, 'Analytics integration', $abilities);
+}
+
+test('admin analytics summary api returns totals daily rows and top products', function (): void {
+    $store = adminAnalyticsSummaryApiStore();
+    $from = now()->subDay()->toDateString();
+    $to = now()->toDateString();
+
+    DB::table('analytics_daily')
+        ->where('store_id', $store->getKey())
+        ->whereBetween('date', [$from, $to])
+        ->delete();
+
+    DB::table('analytics_daily')->insert([
+        [
+            'store_id' => $store->getKey(),
+            'date' => $from,
+            'orders_count' => 2,
+            'revenue_amount' => 2000,
+            'aov_amount' => 1000,
+            'visits_count' => 10,
+            'add_to_cart_count' => 4,
+            'checkout_started_count' => 3,
+            'checkout_completed_count' => 2,
+        ],
+        [
+            'store_id' => $store->getKey(),
+            'date' => $to,
+            'orders_count' => 3,
+            'revenue_amount' => 9000,
+            'aov_amount' => 3000,
+            'visits_count' => 20,
+            'add_to_cart_count' => 6,
+            'checkout_started_count' => 7,
+            'checkout_completed_count' => 3,
+        ],
+    ]);
+
+    $product = Product::factory()
+        ->for($store)
+        ->withDefaultVariant(2000)
+        ->create(['title' => 'Analytics API Jacket']);
+    $order = Order::factory()->paid()->create([
+        'store_id' => $store->getKey(),
+        'currency' => $store->default_currency,
+        'placed_at' => Carbon::parse($from)->addHours(12),
+    ]);
+    OrderLine::factory()->create([
+        'order_id' => $order->getKey(),
+        'product_id' => $product->getKey(),
+        'title_snapshot' => $product->title,
+        'quantity' => 4,
+        'total_amount' => 8000,
+    ]);

-    $response->assertStatus(200);
+    $this->actingAs(adminAnalyticsSummaryApiUser())
+        ->getJson("/api/admin/v1/stores/{$store->getKey()}/analytics/summary?from={$from}&to={$to}")
+        ->assertOk()
+        ->assertJsonPath('data.period.from', $from)
+        ->assertJsonPath('data.period.to', $to)
+        ->assertJsonPath('data.summary.orders_count', 5)
+        ->assertJsonPath('data.summary.revenue_amount', 11000)
+        ->assertJsonPath('data.summary.aov_amount', 2200)
+        ->assertJsonPath('data.summary.visits_count', 30)
+        ->assertJsonPath('data.summary.add_to_cart_count', 10)
+        ->assertJsonPath('data.summary.checkout_started_count', 10)
+        ->assertJsonPath('data.summary.conversion_rate', 0.1667)
+        ->assertJsonPath('data.summary.currency', $store->default_currency)
+        ->assertJsonPath('data.daily.0.date', $from)
+        ->assertJsonPath('data.daily.1.date', $to)
+        ->assertJsonPath('data.top_products.0.product_id', $product->getKey())
+        ->assertJsonPath('data.top_products.0.title', 'Analytics API Jacket')
+        ->assertJsonPath('data.top_products.0.units_sold', 4)
+        ->assertJsonPath('data.top_products.0.revenue_amount', 8000);
+});
+
+test('admin analytics summary api enforces token abilities and store scope', function (): void {
+    $store = adminAnalyticsSummaryApiStore();
+    $otherStore = Store::factory()->create();
+    $from = now()->subDay()->toDateString();
+    $to = now()->toDateString();
+    $readToken = adminAnalyticsSummaryApiToken($store, ['read-analytics']);
+    $wrongAbilityToken = adminAnalyticsSummaryApiToken($store, ['read-settings']);
+    $otherStoreToken = adminAnalyticsSummaryApiToken($otherStore, ['read-analytics']);
+
+    $this->withToken($readToken['plain_text'])
+        ->getJson("/api/admin/v1/stores/{$store->getKey()}/analytics/summary?from={$from}&to={$to}")
+        ->assertOk();
+
+    $this->withToken($wrongAbilityToken['plain_text'])
+        ->getJson("/api/admin/v1/stores/{$store->getKey()}/analytics/summary?from={$from}&to={$to}")
+        ->assertForbidden();
+
+    $this->withToken($otherStoreToken['plain_text'])
+        ->getJson("/api/admin/v1/stores/{$store->getKey()}/analytics/summary?from={$from}&to={$to}")
+        ->assertForbidden();
+});
+
+test('admin analytics summary api validates date ranges', function (): void {
+    $store = adminAnalyticsSummaryApiStore();
+
+    $this->actingAs(adminAnalyticsSummaryApiUser())
+        ->getJson("/api/admin/v1/stores/{$store->getKey()}/analytics/summary?from=2026-02-10&to=2026-02-01")
+        ->assertUnprocessable()
+        ->assertJsonValidationErrors(['to']);
+
+    $this->actingAs(adminAnalyticsSummaryApiUser())
+        ->getJson("/api/admin/v1/stores/{$store->getKey()}/analytics/summary?from=2025-01-01&to=2026-02-01")
+        ->assertUnprocessable()
+        ->assertJsonValidationErrors(['to']);
 });

unified_diff:
@@ -5,2 +5,3 @@
 use App\Http\Controllers\Api\Admin\V1\DiscountController as AdminDiscountController;
+use App\Http\Controllers\Api\Admin\V1\AnalyticsSummaryController as AdminAnalyticsSummaryController;
 use App\Http\Controllers\Api\Admin\V1\OrderController as AdminOrderController;
@@ -106,2 +107,6 @@

+        Route::middleware('admin.api:read-analytics')->group(function (): void {
+            Route::get('analytics/summary', [AdminAnalyticsSummaryController::class, 'show'])->name('analytics.summary');
+        });
+
         Route::middleware('admin.api:read-orders')->group(function (): void {

unified_diff:
@@ -4,4 +4,15 @@

+use App\Enums\FinancialStatus;
 use App\Http\Controllers\Controller;
+use App\Models\OrderLine;
+use App\Models\Store;
+use App\Services\AnalyticsService;
+use Carbon\CarbonInterface;
+use Carbon\CarbonPeriod;
+use Illuminate\Http\JsonResponse;
 use Illuminate\Http\Request;
+use Illuminate\Support\Carbon;
+use Illuminate\Support\Collection;
+use Illuminate\Validation\Rule;
+use Illuminate\Validation\ValidationException;

@@ -9,3 +20,134 @@
 {
-    //
+    public function show(Request $request, Store $store, AnalyticsService $analytics): JsonResponse
+    {
+        $this->authorizeStore($request, $store);
+
+        $validated = $request->validate([
+            'from' => ['required', 'date_format:Y-m-d'],
+            'to' => ['required', 'date_format:Y-m-d', 'after_or_equal:from'],
+            'granularity' => ['nullable', Rule::in(['day', 'week', 'month'])],
+        ]);
+
+        $from = Carbon::createFromFormat('Y-m-d', (string) $validated['from'])->startOfDay();
+        $to = Carbon::createFromFormat('Y-m-d', (string) $validated['to'])->endOfDay();
+
+        if ($from->diffInDays($to) > 365) {
+            throw ValidationException::withMessages([
+                'to' => __('The selected date range may not be greater than 365 days.'),
+            ]);
+        }
+
+        $totals = $analytics->totals($store, $from->toDateString(), $to->toDateString());
+
+        return response()->json([
+            'data' => [
+                'period' => [
+                    'from' => $from->toDateString(),
+                    'to' => $to->toDateString(),
+                ],
+                'summary' => [
+                    'orders_count' => $totals['orders_count'],
+                    'revenue_amount' => $totals['revenue_amount'],
+                    'aov_amount' => $totals['aov_amount'],
+                    'visits_count' => $totals['visits_count'],
+                    'add_to_cart_count' => $totals['add_to_cart_count'],
+                    'checkout_started_count' => $totals['checkout_started_count'],
+                    'conversion_rate' => $totals['visits_count'] > 0 ? round($totals['checkout_completed_count'] / $totals['visits_count'], 4) : 0.0,
+                    'currency' => $store->default_currency,
+                ],
+                'daily' => $this->metricRows($analytics, $store, $from, $to, $validated['granularity'] ?? 'day'),
+                'top_products' => $this->topProducts($store, $from, $to),
+            ],
+        ]);
+    }
+
+    private function authorizeStore(Request $request, Store $store): void
+    {
+        if (! $request->attributes->has('admin_api_oauth_token')) {
+            abort_unless($request->user()?->stores()->whereKey($store->getKey())->exists(), 403);
+        }
+
+        app()->instance('current_store', $store);
+    }
+
+    /**
+     * @return list<array{date: string, orders_count: int, revenue_amount: int, aov_amount: int, visits_count: int, add_to_cart_count: int, checkout_started_count: int}>
+     */
+    private function metricRows(AnalyticsService $analytics, Store $store, CarbonInterface $from, CarbonInterface $to, string $granularity): array
+    {
+        $metrics = $analytics->getDailyMetrics($store, $from->toDateString(), $to->toDateString());
+
+        if ($granularity === 'day') {
+            $metricsByDate = $metrics->keyBy('date');
+
+            return collect(CarbonPeriod::create($from->copy()->startOfDay(), '1 day', $to->copy()->startOfDay()))
+                ->map(fn (CarbonInterface $date): array => $this->metricRow($date->toDateString(), collect([$metricsByDate->get($date->toDateString())])->filter()))
+                ->values()
+                ->all();
+        }
+
+        return $metrics
+            ->groupBy(fn ($metric): string => $this->periodStart((string) $metric->date, $granularity))
+            ->map(fn (Collection $rows, string $date): array => $this->metricRow($date, $rows))
+            ->values()
+            ->all();
+    }
+
+    /**
+     * @param  Collection<int, mixed>  $metrics
+     * @return array{date: string, orders_count: int, revenue_amount: int, aov_amount: int, visits_count: int, add_to_cart_count: int, checkout_started_count: int}
+     */
+    private function metricRow(string $date, Collection $metrics): array
+    {
+        $orders = (int) $metrics->sum('orders_count');
+        $revenue = (int) $metrics->sum('revenue_amount');
+
+        return [
+            'date' => $date,
+            'orders_count' => $orders,
+            'revenue_amount' => $revenue,
+            'aov_amount' => $orders > 0 ? intdiv($revenue, $orders) : 0,
+            'visits_count' => (int) $metrics->sum('visits_count'),
+            'add_to_cart_count' => (int) $metrics->sum('add_to_cart_count'),
+            'checkout_started_count' => (int) $metrics->sum('checkout_started_count'),
+        ];
+    }
+
+    private function periodStart(string $date, string $granularity): string
+    {
+        $date = Carbon::parse($date);
+
+        return match ($granularity) {
+            'week' => $date->startOfWeek()->toDateString(),
+            'month' => $date->startOfMonth()->toDateString(),
+            default => $date->toDateString(),
+        };
+    }
+
+    /**
+     * @return list<array{product_id: int|null, title: string, units_sold: int, revenue_amount: int}>
+     */
+    private function topProducts(Store $store, CarbonInterface $from, CarbonInterface $to): array
+    {
+        return OrderLine::query()
+            ->selectRaw('order_lines.product_id, order_lines.title_snapshot as title, sum(order_lines.quantity) as units_sold, sum(order_lines.total_amount) as revenue_amount')
+            ->join('orders', 'orders.id', '=', 'order_lines.order_id')
+            ->where('orders.store_id', $store->getKey())
+            ->whereBetween('orders.placed_at', [$from, $to])
+            ->whereIn('orders.financial_status', [
+                FinancialStatus::Paid->value,
+                FinancialStatus::PartiallyRefunded->value,
+            ])
+            ->groupBy('order_lines.product_id', 'order_lines.title_snapshot')
+            ->orderByDesc('revenue_amount')
+            ->limit(10)
+            ->get()
+            ->map(fn (OrderLine $line): array => [
+                'product_id' => $line->product_id === null ? null : (int) $line->product_id,
+                'title' => (string) $line->title,
+                'units_sold' => (int) $line->units_sold,
+                'revenue_amount' => (int) $line->revenue_amount,
+            ])
+            ->all();
+    }
 }


output: {"output":"Success. Updated the following files:\nM /Users/fabianwesner/Herd/shop/app/Http/Controllers/Api/Admin/V1/AnalyticsSummaryController.php\nM /Users/fabianwesner/Herd/shop/routes/api.php\nM /Users/fabianwesner/Herd/shop/tests/Feature/Api/AdminAnalyticsSummaryApiTest.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


message: The analytics summary API code is in place. I’m formatting first, then I’ll run the focused analytics API test; date validation and the dynamic top-product query are the areas most likely to need adjustment.


text: The analytics summary API code is in place. I’m formatting first, then I’ll run the focused analytics API test; date validation and the dynamic top-product query are the areas most likely to need adjustment.


output:
Chunk ID: 1e4956
Wall time: 0.3784 seconds
Process exited with code 0
Original token count: 21
Output:
{"result":"fixed","files":[{"path":"routes\/api.php","fixers":["ordered_imports"]}]}


command:
/bin/zsh
-lc
vendor/bin/pint --dirty --format agent

cmd: vendor/bin/pint --dirty --format agent

result: fixed


cmd: php artisan test --compact tests/Feature/Api/AdminAnalyticsSummaryApiTest.php


output:
Chunk ID: 846262
Wall time: 1.0015 seconds
Process running with session ID 46800
Original token count: 0
Output:


command: php artisan test --compact tests/Feature/Api/AdminAnalyticsSummaryApiTest.php

aggregated_output:
  ...

  Tests:    3 passed (26 assertions)
  Duration: 1.34s


output:
Chunk ID: 086e2d
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 16
Output:

  ...

  Tests:    3 passed (26 assertions)
  Duration: 1.34s


message: The focused analytics API test is green: 3 tests, 26 assertions. I’m expanding to the API group and route-list check.


text: The focused analytics API test is green: 3 tests, 26 assertions. I’m expanding to the API group and route-list check.


cmd: php artisan test --compact tests/Feature/Api


cmd: php artisan route:list --path=api/admin/v1/stores --except-vendor