Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.24% covered (success)
90.24%
37 / 41
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
SendPortalLinkAction
90.24% covered (success)
90.24%
37 / 41
50.00% covered (danger)
50.00%
1 / 2
7.05
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
90.00% covered (success)
90.00%
36 / 40
0.00% covered (danger)
0.00%
0 / 1
6.04
1<?php
2
3declare(strict_types=1);
4
5namespace ImovelZapAi\ClientPortal\Application\Actions;
6
7use App\Models\User;
8use ImovelZapAi\ClientPortal\Application\Services\PortalActivityWriter;
9use ImovelZapAi\ClientPortal\Application\Services\PortalTokenService;
10use ImovelZapAi\ClientPortal\Domain\Enums\PortalActivityEvent;
11use ImovelZapAi\ClientPortal\Domain\Events\ClientPortalSent;
12use ImovelZapAi\ClientPortal\Domain\Exceptions\ClientPortalException;
13use ImovelZapAi\ClientPortal\Infrastructure\Persistence\Eloquent\ClientPortalModel;
14use ImovelZapAi\ClientPortal\Infrastructure\Persistence\Eloquent\PortalAccessTokenModel;
15use ImovelZapAi\Conversations\Application\Actions\SendOutboundMessageAction;
16
17final class SendPortalLinkAction
18{
19    public function __construct(
20        private readonly PortalTokenService $tokens,
21        private readonly PortalActivityWriter $activity,
22        private readonly SendOutboundMessageAction $sendText,
23    ) {}
24
25    /**
26     * @return array{url: string, plain_token: string}
27     */
28    public function execute(ClientPortalModel $portal, User $actor, ?string $plainToken = null): array
29    {
30        if (! $portal->isPubliclyAccessible()) {
31            throw ClientPortalException::invalidState('portal não está ativo.');
32        }
33
34        if ($plainToken === null) {
35            $active = PortalAccessTokenModel::query()
36                ->where('client_portal_id', $portal->id)
37                ->whereNull('revoked_at')
38                ->where('expires_at', '>', now())
39                ->latest()
40                ->first();
41
42            if ($active === null) {
43                $issued = $this->tokens->issue($portal, $portal->expires_at);
44                $plainToken = $issued['plain'];
45                $url = $issued['url'];
46            } else {
47                // Cannot recover plain token from hash — reissue
48                $this->tokens->revoke($active);
49                $issued = $this->tokens->issue($portal, $portal->expires_at);
50                $plainToken = $issued['plain'];
51                $url = $issued['url'];
52            }
53        } else {
54            $url = $this->tokens->publicUrl($plainToken);
55        }
56
57        $name = $portal->greeting_name ?: 'olá';
58        $body = "Olá {$name}! Para continuarmos sua negociação, envie os documentos por este link seguro:\n{$url}\n\nO link expira em breve. Qualquer dúvida, fale conosco.";
59
60        $contact = $portal->contact;
61        if ($contact === null) {
62            throw ClientPortalException::invalidState('contato do portal não encontrado.');
63        }
64
65        $this->sendText->execute(
66            $portal->tenant_id,
67            $contact,
68            $body,
69            $portal->conversation,
70        );
71
72        $this->activity->write(
73            $portal->tenant_id,
74            $portal->id,
75            PortalActivityEvent::PortalSent,
76            actorType: 'broker',
77            actorUserId: (int) $actor->id,
78            message: 'Link do portal enviado por WhatsApp.',
79            payload: ['url' => $url],
80        );
81
82        ClientPortalSent::dispatch($portal->tenant_id, $portal->id);
83
84        return ['url' => $url, 'plain_token' => $plainToken];
85    }
86}