Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
33 / 33
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
AgendaConflictChecker
100.00% covered (success)
100.00%
33 / 33
100.00% covered (success)
100.00%
2 / 2
5
100.00% covered (success)
100.00%
1 / 1
 assertNoConflict
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
4
 settingsFor
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace ImovelZapAi\Visits\Application\Services;
6
7use Carbon\CarbonInterface;
8use ImovelZapAi\Visits\Domain\Enums\VisitStatus;
9use ImovelZapAi\Visits\Domain\Exceptions\VisitException;
10use ImovelZapAi\Visits\Infrastructure\Persistence\Eloquent\TenantVisitSettingsModel;
11use ImovelZapAi\Visits\Infrastructure\Persistence\Eloquent\VisitModel;
12
13final class AgendaConflictChecker
14{
15    public function assertNoConflict(
16        string $tenantId,
17        int $responsibleUserId,
18        CarbonInterface $start,
19        CarbonInterface $end,
20        ?string $ignoreVisitId = null,
21    ): void {
22        $settings = $this->settingsFor($tenantId);
23
24        if ($settings->allow_overlap) {
25            return;
26        }
27
28        $buffer = (int) $settings->conflict_buffer_minutes;
29        $windowStart = $start->copy()->subMinutes($buffer);
30        $windowEnd = $end->copy()->addMinutes($buffer);
31
32        $query = VisitModel::query()
33            ->where('tenant_id', $tenantId)
34            ->where('responsible_user_id', $responsibleUserId)
35            ->whereNotIn('status', [
36                VisitStatus::Cancelled->value,
37                VisitStatus::Completed->value,
38                VisitStatus::NoShow->value,
39            ])
40            ->whereNotNull('scheduled_start')
41            ->whereNotNull('scheduled_end')
42            ->where('scheduled_start', '<', $windowEnd)
43            ->where('scheduled_end', '>', $windowStart);
44
45        if ($ignoreVisitId !== null) {
46            $query->whereKeyNot($ignoreVisitId);
47        }
48
49        $conflict = $query->first();
50
51        if ($conflict !== null) {
52            throw VisitException::scheduleConflict(
53                "já existe visita {$conflict->id} entre {$conflict->scheduled_start} e {$conflict->scheduled_end}."
54            );
55        }
56    }
57
58    public function settingsFor(string $tenantId): TenantVisitSettingsModel
59    {
60        return TenantVisitSettingsModel::query()->firstOrCreate(
61            ['tenant_id' => $tenantId],
62            [
63                'default_duration_minutes' => (int) config('visits.default_duration_minutes', 60),
64                'conflict_buffer_minutes' => (int) config('visits.conflict_buffer_minutes', 15),
65                'allow_overlap' => (bool) config('visits.allow_overlap', false),
66            ],
67        );
68    }
69}