Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.43% covered (success)
96.43%
54 / 56
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
ScheduleListingVisitAction
96.43% covered (success)
96.43%
54 / 56
66.67% covered (warning)
66.67%
2 / 3
5
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
94.74% covered (success)
94.74%
36 / 38
0.00% covered (danger)
0.00%
0 / 1
3.00
 resolveContactId
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace ImovelZapAi\Owners\Application\Actions;
6
7use App\Models\User;
8use Carbon\Carbon;
9use Illuminate\Support\Facades\DB;
10use ImovelZapAi\Conversations\Infrastructure\Persistence\Eloquent\ContactModel;
11use ImovelZapAi\Owners\Application\Services\ListingHistoryWriter;
12use ImovelZapAi\Owners\Domain\Enums\ListingHistoryEvent;
13use ImovelZapAi\Owners\Domain\Enums\ListingStageKey;
14use ImovelZapAi\Owners\Domain\Exceptions\OwnerException;
15use ImovelZapAi\Owners\Infrastructure\Persistence\Eloquent\ListingOpportunityModel;
16use ImovelZapAi\Owners\Infrastructure\Persistence\Eloquent\ListingVisitModel;
17use ImovelZapAi\Owners\Infrastructure\Persistence\Eloquent\OwnerContactModel;
18use ImovelZapAi\Visits\Application\Actions\CreateVisitRequestAction;
19use ImovelZapAi\Visits\Application\Actions\ScheduleVisitAction;
20use ImovelZapAi\Visits\Domain\Enums\VisitPurpose;
21
22final class ScheduleListingVisitAction
23{
24    public function __construct(
25        private readonly CreateVisitRequestAction $createVisit,
26        private readonly ScheduleVisitAction $scheduleVisit,
27        private readonly ChangeListingStageAction $changeStage,
28        private readonly ListingHistoryWriter $history,
29    ) {}
30
31    /**
32     * @param  array{
33     *     scheduled_start: string,
34     *     scheduled_end?: string|null,
35     *     purpose?: string|null,
36     *     location_notes?: string|null,
37     * }  $data
38     */
39    public function execute(ListingOpportunityModel $opportunity, User $actor, array $data): ListingVisitModel
40    {
41        if ($opportunity->isClosed()) {
42            throw OwnerException::invalidState('oportunidade encerrada.');
43        }
44
45        return DB::transaction(function () use ($opportunity, $actor, $data): ListingVisitModel {
46            $purpose = VisitPurpose::tryFrom((string) ($data['purpose'] ?? VisitPurpose::ListingEvaluation->value))
47                ?? VisitPurpose::ListingEvaluation;
48
49            $contactId = $this->resolveContactId($opportunity);
50
51            $visit = $this->createVisit->execute($opportunity->tenant_id, $actor, [
52                'contact_id' => $contactId,
53                'property_id' => $opportunity->property_id,
54                'owner_id' => $opportunity->owner_id,
55                'listing_opportunity_id' => $opportunity->id,
56                'purpose' => $purpose->value,
57                'location_notes' => $data['location_notes'] ?? null,
58            ]);
59
60            $start = Carbon::parse($data['scheduled_start']);
61            $end = isset($data['scheduled_end']) ? Carbon::parse($data['scheduled_end']) : null;
62            $visit = $this->scheduleVisit->execute($visit, $actor, $start, $end);
63
64            $link = ListingVisitModel::query()->create([
65                'tenant_id' => $opportunity->tenant_id,
66                'listing_opportunity_id' => $opportunity->id,
67                'visit_id' => $visit->id,
68                'owner_id' => $opportunity->owner_id,
69                'purpose' => $purpose,
70            ]);
71
72            $opportunity->forceFill(['last_activity_at' => now()])->save();
73            $opportunity->owner?->forceFill(['last_contacted_at' => now()])->save();
74
75            $this->changeStage->execute($opportunity->fresh(), $actor, ListingStageKey::EvaluationScheduled->value);
76
77            $this->history->write(
78                $opportunity->tenant_id,
79                ListingHistoryEvent::VisitLinked,
80                opportunityId: $opportunity->id,
81                ownerId: $opportunity->owner_id,
82                actorUserId: (int) $actor->id,
83                after: ['visit_id' => $visit->id, 'purpose' => $purpose->value],
84                message: 'Visita de captação agendada.',
85            );
86
87            return $link->fresh(['visit']);
88        });
89    }
90
91    private function resolveContactId(ListingOpportunityModel $opportunity): string
92    {
93        $phone = OwnerContactModel::query()
94            ->where('owner_id', $opportunity->owner_id)
95            ->whereIn('channel', ['phone', 'whatsapp'])
96            ->orderByDesc('is_primary')
97            ->first();
98
99        $waId = $phone?->value ?? ('owner-'.$opportunity->owner_id);
100
101        $contact = ContactModel::query()->firstOrCreate(
102            [
103                'tenant_id' => $opportunity->tenant_id,
104                'wa_id' => $waId,
105            ],
106            [
107                'phone' => $waId,
108                'display_name' => $opportunity->owner?->name ?? 'Proprietário',
109            ],
110        );
111
112        return $contact->id;
113    }
114}