Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.21% covered (success)
98.21%
55 / 56
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
ScheduleVisitAction
98.21% covered (success)
98.21%
55 / 56
66.67% covered (warning)
66.67%
2 / 3
10
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
 execute
100.00% covered (success)
100.00%
44 / 44
100.00% covered (success)
100.00%
1 / 1
5
 syncInterest
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
4.01
1<?php
2
3declare(strict_types=1);
4
5namespace ImovelZapAi\Visits\Application\Actions;
6
7use App\Models\User;
8use Carbon\Carbon;
9use Illuminate\Support\Facades\DB;
10use ImovelZapAi\Conversations\Application\Actions\UpdatePersonInterestStageAction;
11use ImovelZapAi\Properties\Domain\Enums\PropertyInterestStatus;
12use ImovelZapAi\Properties\Infrastructure\Persistence\Eloquent\PropertyInterestModel;
13use ImovelZapAi\Visits\Application\Services\AgendaConflictChecker;
14use ImovelZapAi\Visits\Application\Services\VisitLogWriter;
15use ImovelZapAi\Visits\Application\Services\VisitReminderScheduler;
16use ImovelZapAi\Visits\Application\Services\VisitSnapshotter;
17use ImovelZapAi\Visits\Domain\Enums\VisitLogEvent;
18use ImovelZapAi\Visits\Domain\Enums\VisitStatus;
19use ImovelZapAi\Visits\Domain\Events\VisitConfirmed;
20use ImovelZapAi\Visits\Domain\Exceptions\VisitException;
21use ImovelZapAi\Visits\Infrastructure\Persistence\Eloquent\VisitModel;
22
23final class ScheduleVisitAction
24{
25    public function __construct(
26        private readonly AgendaConflictChecker $conflictChecker,
27        private readonly VisitReminderScheduler $reminderScheduler,
28        private readonly VisitLogWriter $logWriter,
29        private readonly VisitSnapshotter $snapshotter,
30        private readonly UpdatePersonInterestStageAction $updateInterestStage,
31    ) {}
32
33    public function execute(
34        VisitModel $visit,
35        User $actor,
36        Carbon $start,
37        ?Carbon $end = null,
38        ?int $responsibleUserId = null,
39        bool $autoConfirm = true,
40    ): VisitModel {
41        if ($visit->status->isTerminal()) {
42            throw VisitException::invalidState($visit->status->value, 'schedule');
43        }
44
45        $settings = $this->conflictChecker->settingsFor($visit->tenant_id);
46        $end ??= $start->copy()->addMinutes((int) $settings->default_duration_minutes);
47        $brokerId = $responsibleUserId ?? (int) ($visit->responsible_user_id ?? $actor->id);
48
49        $this->conflictChecker->assertNoConflict(
50            $visit->tenant_id,
51            $brokerId,
52            $start,
53            $end,
54            $visit->id,
55        );
56
57        return DB::transaction(function () use ($visit, $actor, $start, $end, $brokerId, $autoConfirm): VisitModel {
58            $before = $this->snapshotter->snapshot($visit);
59
60            $status = $autoConfirm ? VisitStatus::Confirmed : VisitStatus::Scheduled;
61
62            $visit->forceFill([
63                'responsible_user_id' => $brokerId,
64                'scheduled_start' => $start,
65                'scheduled_end' => $end,
66                'status' => $status,
67                'confirmed_at' => $autoConfirm ? now() : null,
68            ])->save();
69
70            $this->syncInterest($visit);
71
72            $this->logWriter->write(
73                $visit->tenant_id,
74                $visit->id,
75                VisitLogEvent::Scheduled,
76                actorUserId: (int) $actor->id,
77                before: $before,
78                after: $this->snapshotter->snapshot($visit->fresh()),
79                message: 'Visita agendada.',
80            );
81
82            if ($autoConfirm) {
83                $this->logWriter->write(
84                    $visit->tenant_id,
85                    $visit->id,
86                    VisitLogEvent::Confirmed,
87                    actorUserId: (int) $actor->id,
88                    message: 'Visita confirmada automaticamente.',
89                );
90                VisitConfirmed::dispatch($visit->tenant_id, $visit->id);
91            }
92
93            $this->reminderScheduler->scheduleForVisit($visit->fresh(), sendInitialNow: $autoConfirm);
94
95            return $visit->fresh(['participants', 'reminders', 'contact', 'property']);
96        });
97    }
98
99    private function syncInterest(VisitModel $visit): void
100    {
101        if ($visit->property_interest_id === null) {
102            return;
103        }
104
105        $interest = PropertyInterestModel::query()->find($visit->property_interest_id);
106        if ($interest === null) {
107            return;
108        }
109
110        if ($interest->status->canTransitionTo(PropertyInterestStatus::VisitScheduled)) {
111            $this->updateInterestStage->execute(
112                $visit->tenant_id,
113                $interest,
114                PropertyInterestStatus::VisitScheduled,
115            );
116        }
117    }
118}