Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
94 / 94
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
RunOwnerAlertsAction
100.00% covered (success)
100.00%
94 / 94
100.00% covered (success)
100.00%
2 / 2
2
100.00% covered (success)
100.00%
1 / 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%
93 / 93
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 ImovelZapAi\Owners\Application\Services\ListingHistoryWriter;
9use ImovelZapAi\Owners\Domain\Enums\ListingAgreementStatus;
10use ImovelZapAi\Owners\Domain\Enums\ListingHistoryEvent;
11use ImovelZapAi\Owners\Domain\Enums\ListingStageKey;
12use ImovelZapAi\Owners\Domain\Events\ListingAgreementExpiring;
13use ImovelZapAi\Owners\Infrastructure\Persistence\Eloquent\ListingAgreementModel;
14use ImovelZapAi\Owners\Infrastructure\Persistence\Eloquent\ListingOpportunityModel;
15use ImovelZapAi\Owners\Infrastructure\Persistence\Eloquent\OwnerModel;
16
17final class RunOwnerAlertsAction
18{
19    public function __construct(
20        private readonly ListingHistoryWriter $history,
21    ) {}
22
23    /**
24     * @return array{expiring_agreements: int, stagnant_owners: int, pending_publish: int, pending_evaluation: int}
25     */
26    public function execute(string $tenantId, ?User $actor = null): array
27    {
28        $actorId = $actor?->id;
29        $expiring = 0;
30        $stagnantOwners = 0;
31        $pendingPublish = 0;
32        $pendingEvaluation = 0;
33
34        $days = (int) config('owners.agreement_expiry_alert_days', 30);
35        $until = now()->addDays($days)->toDateString();
36
37        ListingAgreementModel::query()
38            ->where('tenant_id', $tenantId)
39            ->where('status', ListingAgreementStatus::Active)
40            ->whereDate('ends_on', '<=', $until)
41            ->whereDate('ends_on', '>=', now()->toDateString())
42            ->whereNull('expiry_alerted_at')
43            ->limit(50)
44            ->get()
45            ->each(function (ListingAgreementModel $agreement) use ($tenantId, $actorId, &$expiring): void {
46                $agreement->forceFill(['expiry_alerted_at' => now()])->save();
47                $this->history->write(
48                    $tenantId,
49                    ListingHistoryEvent::Alert,
50                    opportunityId: $agreement->listing_opportunity_id,
51                    ownerId: $agreement->owner_id,
52                    agreementId: $agreement->id,
53                    actorUserId: $actorId,
54                    message: 'Contrato vence em breve (alerta automático).',
55                );
56                ListingAgreementExpiring::dispatch($tenantId, $agreement->id);
57                $expiring++;
58            });
59
60        $stagnationDays = (int) config('owners.owner_stagnation_days', 14);
61        OwnerModel::query()
62            ->where('tenant_id', $tenantId)
63            ->where(function ($q) use ($stagnationDays): void {
64                $cutoff = now()->subDays($stagnationDays);
65                $q->where('last_contacted_at', '<=', $cutoff)
66                    ->orWhereNull('last_contacted_at');
67            })
68            ->limit(50)
69            ->get()
70            ->each(function (OwnerModel $owner) use ($tenantId, $actorId, &$stagnantOwners): void {
71                $this->history->write(
72                    $tenantId,
73                    ListingHistoryEvent::Alert,
74                    ownerId: $owner->id,
75                    actorUserId: $actorId,
76                    message: 'Proprietário sem contato recente.',
77                );
78                $stagnantOwners++;
79            });
80
81        $prePublishDays = (int) config('owners.pre_publish_stagnation_days', 7);
82        ListingOpportunityModel::query()
83            ->where('tenant_id', $tenantId)
84            ->whereNull('closed_at')
85            ->whereHas('stage', fn ($q) => $q->where('key', ListingStageKey::Captured->value))
86            ->where('stage_entered_at', '<=', now()->subDays($prePublishDays))
87            ->limit(50)
88            ->get()
89            ->each(function (ListingOpportunityModel $opp) use ($tenantId, $actorId, &$pendingPublish): void {
90                $this->history->write(
91                    $tenantId,
92                    ListingHistoryEvent::Alert,
93                    opportunityId: $opp->id,
94                    ownerId: $opp->owner_id,
95                    actorUserId: $actorId,
96                    message: 'Imóvel parado antes da publicação.',
97                );
98                $pendingPublish++;
99            });
100
101        ListingOpportunityModel::query()
102            ->where('tenant_id', $tenantId)
103            ->whereNull('closed_at')
104            ->whereHas('stage', fn ($q) => $q->where('key', ListingStageKey::NewContact->value))
105            ->whereNull('agency_recommended_price')
106            ->where('created_at', '<=', now()->subDays(3))
107            ->limit(50)
108            ->get()
109            ->each(function (ListingOpportunityModel $opp) use ($tenantId, $actorId, &$pendingEvaluation): void {
110                $this->history->write(
111                    $tenantId,
112                    ListingHistoryEvent::Alert,
113                    opportunityId: $opp->id,
114                    ownerId: $opp->owner_id,
115                    actorUserId: $actorId,
116                    message: 'Avaliação pendente.',
117                );
118                $pendingEvaluation++;
119            });
120
121        return [
122            'expiring_agreements' => $expiring,
123            'stagnant_owners' => $stagnantOwners,
124            'pending_publish' => $pendingPublish,
125            'pending_evaluation' => $pendingEvaluation,
126        ];
127    }
128}