Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.87% covered (success)
94.87%
37 / 39
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
VisitReminderScheduler
94.87% covered (success)
94.87%
37 / 39
66.67% covered (warning)
66.67%
2 / 3
11.02
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 scheduleForVisit
94.12% covered (success)
94.12%
32 / 34
0.00% covered (danger)
0.00%
0 / 1
9.02
 cancelPending
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace ImovelZapAi\Visits\Application\Services;
6
7use ImovelZapAi\Visits\Domain\Enums\VisitReminderStatus;
8use ImovelZapAi\Visits\Domain\Enums\VisitReminderType;
9use ImovelZapAi\Visits\Infrastructure\Jobs\SendVisitReminderJob;
10use ImovelZapAi\Visits\Infrastructure\Persistence\Eloquent\VisitModel;
11use ImovelZapAi\Visits\Infrastructure\Persistence\Eloquent\VisitReminderModel;
12
13final class VisitReminderScheduler
14{
15    public function __construct(
16        private readonly VisitMessageRenderer $renderer,
17    ) {}
18
19    public function scheduleForVisit(VisitModel $visit, bool $sendInitialNow = true): void
20    {
21        if ($visit->scheduled_start === null) {
22            return;
23        }
24
25        $this->renderer->ensureDefaultTemplates($visit->tenant_id);
26        $this->cancelPending($visit->id);
27
28        $start = $visit->scheduled_start;
29
30        $plan = [
31            VisitReminderType::InitialConfirmation->value => $sendInitialNow ? now() : $start->copy()->subMinutes(5),
32            VisitReminderType::Reminder24h->value => $start->copy()->subHours((int) config('visits.reminders.hours_before_24h', 24)),
33            VisitReminderType::Reminder2h->value => $start->copy()->subHours((int) config('visits.reminders.hours_before_2h', 2)),
34            VisitReminderType::PresenceConfirmation->value => $start->copy()->subMinutes(30),
35        ];
36
37        foreach ($plan as $typeValue => $when) {
38            $type = VisitReminderType::from($typeValue);
39
40            if ($this->renderer->templateBody($visit->tenant_id, $type) === null) {
41                continue;
42            }
43
44            if ($when->lessThanOrEqualTo(now()->subMinute()) && $type !== VisitReminderType::InitialConfirmation) {
45                continue;
46            }
47
48            $reminder = VisitReminderModel::query()->updateOrCreate(
49                [
50                    'visit_id' => $visit->id,
51                    'type' => $type,
52                ],
53                [
54                    'tenant_id' => $visit->tenant_id,
55                    'status' => VisitReminderStatus::Pending,
56                    'scheduled_for' => $when->greaterThan(now()) ? $when : now(),
57                    'sent_at' => null,
58                    'message_body' => null,
59                    'error_message' => null,
60                ],
61            );
62
63            if ($reminder->scheduled_for->lessThanOrEqualTo(now()->addMinute())) {
64                SendVisitReminderJob::dispatch($reminder->id, $visit->tenant_id)
65                    ->onQueue((string) config('visits.queue', 'visits'));
66            }
67        }
68    }
69
70    public function cancelPending(string $visitId): void
71    {
72        VisitReminderModel::query()
73            ->where('visit_id', $visitId)
74            ->where('status', VisitReminderStatus::Pending)
75            ->update(['status' => VisitReminderStatus::Cancelled]);
76    }
77}