Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.49% covered (warning)
86.49%
64 / 74
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
DecideProposalAction
86.49% covered (warning)
86.49%
64 / 74
50.00% covered (danger)
50.00%
1 / 2
18.80
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
86.30% covered (warning)
86.30%
63 / 73
0.00% covered (danger)
0.00%
0 / 1
17.74
1<?php
2
3declare(strict_types=1);
4
5namespace ImovelZapAi\Proposals\Application\Actions;
6
7use App\Models\User;
8use Illuminate\Support\Facades\DB;
9use ImovelZapAi\Proposals\Application\Services\ProposalHistoryWriter;
10use ImovelZapAi\Proposals\Application\Services\ProposalShareTokenService;
11use ImovelZapAi\Proposals\Domain\Enums\ProposalDecisionType;
12use ImovelZapAi\Proposals\Domain\Enums\ProposalHistoryEvent;
13use ImovelZapAi\Proposals\Domain\Enums\ProposalStatus;
14use ImovelZapAi\Proposals\Domain\Events\ProposalAccepted;
15use ImovelZapAi\Proposals\Domain\Events\ProposalRejected;
16use ImovelZapAi\Proposals\Domain\Exceptions\ProposalException;
17use ImovelZapAi\Proposals\Infrastructure\Persistence\Eloquent\ProposalCommentModel;
18use ImovelZapAi\Proposals\Infrastructure\Persistence\Eloquent\ProposalDecisionModel;
19use ImovelZapAi\Proposals\Infrastructure\Persistence\Eloquent\ProposalModel;
20use ImovelZapAi\Sales\Application\Actions\ChangeDealStageAction;
21use ImovelZapAi\Sales\Domain\Enums\DealLostReason;
22use ImovelZapAi\Sales\Domain\Enums\DealStageKey;
23
24final class DecideProposalAction
25{
26    public function __construct(
27        private readonly ProposalShareTokenService $tokens,
28        private readonly ProposalHistoryWriter $history,
29        private readonly ChangeDealStageAction $changeDealStage,
30    ) {}
31
32    public function execute(
33        string $plainToken,
34        string $decision,
35        ?string $comment = null,
36        ?string $decidedByName = null,
37        ?string $ip = null,
38    ): ProposalModel {
39        $decisionEnum = ProposalDecisionType::tryFrom($decision)
40            ?? throw ProposalException::invalidState('decisão inválida.');
41
42        $resolved = $this->tokens->resolve($plainToken);
43        $proposal = $resolved['proposal'];
44
45        return DB::transaction(function () use ($proposal, $decisionEnum, $comment, $decidedByName, $ip): ProposalModel {
46            if ($proposal->current_version_id === null) {
47                throw ProposalException::invalidState('sem versão.');
48            }
49
50            ProposalDecisionModel::query()->create([
51                'tenant_id' => $proposal->tenant_id,
52                'proposal_id' => $proposal->id,
53                'proposal_version_id' => $proposal->current_version_id,
54                'decision' => $decisionEnum,
55                'comment' => $comment,
56                'decided_by_name' => $decidedByName ?? $proposal->contact?->display_name,
57                'ip' => $ip,
58                'created_at' => now(),
59            ]);
60
61            if ($comment !== null && trim($comment) !== '') {
62                ProposalCommentModel::query()->create([
63                    'tenant_id' => $proposal->tenant_id,
64                    'proposal_id' => $proposal->id,
65                    'proposal_version_id' => $proposal->current_version_id,
66                    'author_type' => 'client',
67                    'author_name' => $decidedByName ?? $proposal->contact?->display_name,
68                    'body' => $comment,
69                    'visible_to_client' => true,
70                ]);
71                $this->history->write(
72                    $proposal->tenant_id,
73                    $proposal->id,
74                    ProposalHistoryEvent::CommentAdded,
75                    actorType: 'client',
76                    message: 'Comentário do cliente na proposta.',
77                );
78            }
79
80            $status = match ($decisionEnum) {
81                ProposalDecisionType::Accepted => ProposalStatus::Accepted,
82                ProposalDecisionType::Rejected => ProposalStatus::Rejected,
83                ProposalDecisionType::ChangesRequested => ProposalStatus::ChangesRequested,
84            };
85
86            $proposal->forceFill([
87                'status' => $status,
88                'decided_at' => $decisionEnum === ProposalDecisionType::ChangesRequested ? null : now(),
89            ])->save();
90
91            $deal = $proposal->deal;
92            $systemUser = User::query()->where('tenant_id', $proposal->tenant_id)->orderBy('id')->first();
93
94            if ($deal !== null && $systemUser !== null && $deal->closed_at === null) {
95                try {
96                    match ($decisionEnum) {
97                        ProposalDecisionType::Accepted => $this->changeDealStage->execute(
98                            $deal,
99                            $systemUser,
100                            DealStageKey::Documentation->value,
101                        ),
102                        ProposalDecisionType::Rejected => $this->changeDealStage->execute(
103                            $deal,
104                            $systemUser,
105                            DealStageKey::Lost->value,
106                            DealLostReason::Other->value,
107                            $comment ?? 'Proposta recusada',
108                        ),
109                        ProposalDecisionType::ChangesRequested => $this->changeDealStage->execute(
110                            $deal,
111                            $systemUser,
112                            DealStageKey::Negotiation->value,
113                        ),
114                    };
115                } catch (\Throwable) {
116                }
117            }
118
119            if ($decisionEnum === ProposalDecisionType::Accepted) {
120                $this->history->write($proposal->tenant_id, $proposal->id, ProposalHistoryEvent::Accepted, actorType: 'client', message: 'Proposta aceita.');
121                ProposalAccepted::dispatch($proposal->tenant_id, $proposal->id);
122            } elseif ($decisionEnum === ProposalDecisionType::Rejected) {
123                $this->history->write($proposal->tenant_id, $proposal->id, ProposalHistoryEvent::Rejected, actorType: 'client', message: 'Proposta recusada.');
124                ProposalRejected::dispatch($proposal->tenant_id, $proposal->id);
125            } else {
126                $this->history->write($proposal->tenant_id, $proposal->id, ProposalHistoryEvent::ChangesRequested, actorType: 'client', message: 'Cliente solicitou alterações.');
127            }
128
129            return $proposal->fresh(['currentVersion', 'decisions', 'comments']);
130        });
131    }
132}