Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.12% covered (success)
95.12%
39 / 41
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
NativeSignerCompletionService
95.12% covered (success)
95.12%
39 / 41
66.67% covered (warning)
66.67%
2 / 3
15
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
 complete
94.12% covered (success)
94.12%
32 / 34
0.00% covered (danger)
0.00%
0 / 1
8.01
 detectDevice
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2
3declare(strict_types=1);
4
5namespace ImovelZapAi\Signatures\Application\Services;
6
7use ImovelZapAi\Signatures\Domain\Enums\SignerStatus;
8use ImovelZapAi\Signatures\Domain\Exceptions\SignatureException;
9use ImovelZapAi\Signatures\Infrastructure\Persistence\Eloquent\SignatureEvidenceModel;
10use ImovelZapAi\Signatures\Infrastructure\Persistence\Eloquent\SignatureRequestModel;
11use ImovelZapAi\Signatures\Infrastructure\Persistence\Eloquent\SignerModel;
12
13final class NativeSignerCompletionService
14{
15    public function __construct(private readonly SignatureOtpService $otp) {}
16
17    /** @param  array<string, mixed>  $payload */
18    public function complete(SignatureRequestModel $request, SignerModel $signer, array $payload): void
19    {
20        if (! $request->isOpen()) {
21            throw SignatureException::invalidState('solicitação encerrada.');
22        }
23
24        if ($signer->status === SignerStatus::Signed) {
25            throw SignatureException::invalidState('signatário já assinou.');
26        }
27
28        $typedName = trim((string) ($payload['typed_name'] ?? ''));
29        $otp = (string) ($payload['otp'] ?? '');
30        $ip = isset($payload['ip']) ? (string) $payload['ip'] : null;
31        $userAgent = isset($payload['user_agent']) ? substr((string) $payload['user_agent'], 0, 512) : null;
32
33        if ($typedName === '') {
34            throw SignatureException::invalidState('nome digitado é obrigatório.');
35        }
36
37        if (! $this->otp->verify($signer, $otp)) {
38            throw SignatureException::otpInvalid();
39        }
40
41        $device = $this->detectDevice($userAgent);
42        $now = now();
43
44        foreach (['button' => ['action' => 'sign'], 'typed_name' => ['name' => $typedName], 'otp' => ['verified' => true]] as $kind => $evidencePayload) {
45            SignatureEvidenceModel::query()->create([
46                'tenant_id' => $request->tenant_id,
47                'signature_request_id' => $request->id,
48                'signer_id' => $signer->id,
49                'kind' => $kind,
50                'payload' => $evidencePayload,
51                'ip' => $ip,
52                'user_agent' => $userAgent,
53                'device' => $device,
54                'created_at' => $now,
55            ]);
56        }
57
58        $signer->forceFill([
59            'status' => SignerStatus::Signed,
60            'typed_name' => $typedName,
61            'signed_at' => $now,
62            'otp_hash' => null,
63            'otp_expires_at' => null,
64            'otp_attempts' => 0,
65        ])->save();
66    }
67
68    private function detectDevice(?string $userAgent): string
69    {
70        $ua = strtolower((string) $userAgent);
71        if (str_contains($ua, 'mobile') || str_contains($ua, 'android') || str_contains($ua, 'iphone')) {
72            return 'mobile';
73        }
74        if (str_contains($ua, 'tablet') || str_contains($ua, 'ipad')) {
75            return 'tablet';
76        }
77
78        return 'desktop';
79    }
80}