Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
GetFunnelQuery
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 2
72
0.00% covered (danger)
0.00%
0 / 1
 execute
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
30
 period
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3declare(strict_types=1);
4
5namespace ImovelZapAi\Analytics\Application\Queries;
6
7use Illuminate\Support\Carbon;
8use Illuminate\Support\Facades\DB;
9use ImovelZapAi\Analytics\Domain\Enums\AnalyticsEventKey;
10use ImovelZapAi\Analytics\Infrastructure\Persistence\Eloquent\AnalyticsFactModel;
11
12final class GetFunnelQuery
13{
14    /** @var list<array{key: string, label: string, event_key: string}> */
15    private const STAGES = [
16        ['key' => 'opportunities', 'label' => 'Oportunidades', 'event_key' => AnalyticsEventKey::DealCreated->value],
17        ['key' => 'visits', 'label' => 'Visitas agendadas', 'event_key' => AnalyticsEventKey::VisitCreated->value],
18        ['key' => 'visits_done', 'label' => 'Visitas concluĂ­das', 'event_key' => AnalyticsEventKey::VisitCompleted->value],
19        ['key' => 'signatures', 'label' => 'Assinaturas solicitadas', 'event_key' => AnalyticsEventKey::SignatureRequested->value],
20        ['key' => 'signatures_done', 'label' => 'Assinaturas concluĂ­das', 'event_key' => AnalyticsEventKey::SignatureCompleted->value],
21        ['key' => 'won', 'label' => 'Oportunidades ganhas', 'event_key' => AnalyticsEventKey::DealWon->value],
22    ];
23
24    /**
25     * @param  array{from?: string, to?: string, days?: int}  $filters
26     * @return array<string, mixed>
27     */
28    public function execute(string $tenantId, array $filters = []): array
29    {
30        [$from, $to] = $this->period($filters);
31
32        $counts = AnalyticsFactModel::query()
33            ->select('event_key', DB::raw('COUNT(*) as total'))
34            ->where('tenant_id', $tenantId)
35            ->whereBetween('occurred_at', [$from, $to])
36            ->whereIn('event_key', array_column(self::STAGES, 'event_key'))
37            ->groupBy('event_key')
38            ->pluck('total', 'event_key');
39
40        $stages = [];
41        $previousCount = null;
42
43        foreach (self::STAGES as $stage) {
44            $count = (int) ($counts[$stage['event_key']] ?? 0);
45            $conversion = $previousCount !== null && $previousCount > 0
46                ? round(($count / $previousCount) * 100, 1)
47                : null;
48
49            $stages[] = [
50                'key' => $stage['key'],
51                'label' => $stage['label'],
52                'count' => $count,
53                'conversion_from_previous' => $conversion,
54            ];
55
56            $previousCount = $count > 0 ? $count : $previousCount;
57        }
58
59        return [
60            'period' => ['from' => $from->toDateString(), 'to' => $to->toDateString()],
61            'stages' => $stages,
62        ];
63    }
64
65    /** @param array{from?: string, to?: string, days?: int} $filters */
66    /** @return array{0: Carbon, 1: Carbon} */
67    private function period(array $filters): array
68    {
69        $to = isset($filters['to']) ? Carbon::parse($filters['to'])->endOfDay() : now();
70        $from = isset($filters['from'])
71            ? Carbon::parse($filters['from'])->startOfDay()
72            : $to->copy()->subDays((int) ($filters['days'] ?? 30))->startOfDay();
73
74        return [$from, $to];
75    }
76}