| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259 |
- <?php
- namespace App\Services\Didit;
- use App\Enums\IdentityVerificationStatusEnum;
- use App\Enums\UserTypeEnum;
- use App\Models\Client;
- use App\Models\IdentityVerification;
- use App\Models\Provider;
- use App\Models\User;
- use App\Services\ProviderService;
- use Illuminate\Database\Eloquent\Model;
- use Illuminate\Support\Facades\DB;
- use Illuminate\Support\Facades\Log;
- class DiditDecisionService
- {
- public function __construct(
- private readonly DiditDecisionExtractor $extractor,
- private readonly ProviderService $providerService,
- ) {}
- public function handleWebhook(array $payload): ?IdentityVerification
- {
- $sessionId = data_get($payload, 'session_id');
- $status = (string) data_get($payload, 'status');
- if (! is_string($sessionId) || $sessionId === '') {
- Log::channel('didit')->warning('Webhook sem session_id; ignorado');
- return null;
- }
- $user = $this->resolveUser($payload);
- if (! $user) {
- Log::channel('didit')->warning('Webhook sem usuario correspondente', [
- 'session_id' => $sessionId,
- 'vendor_data' => data_get($payload, 'vendor_data'),
- ]);
- return null;
- }
- $decision = (array) (data_get($payload, 'decision') ?? []);
- $verification = $this->persist($user, $sessionId, $status, $payload, $decision);
- if (! in_array($status, IdentityVerificationStatusEnum::diditTerminalStatuses(), true)) {
- return $verification;
- }
- $this->applyDecision($user, $verification, $status, $decision);
- return $verification->refresh();
- }
- //
- private function persist(
- User $user,
- string $sessionId,
- string $status,
- array $payload,
- array $decision,
- ): IdentityVerification {
- $verification = IdentityVerification::firstOrNew(['session_id' => $sessionId]);
- $attributes = [
- 'user_id' => $user->id,
- 'didit_status' => $status,
- 'workflow_id' => data_get($payload, 'workflow_id') ?? $verification->workflow_id,
- ];
- if ($decision !== []) {
- $attributes = [
- ...$attributes,
- ...$this->extractor->extract($decision),
- 'decision' => $this->sanitize($decision),
- ];
- }
- if (! $verification->exists) {
- $attributes['attempt'] = (int) $user->identityVerifications()->count() + 1;
- $attributes['started_at'] = now();
- }
- if (in_array($status, IdentityVerificationStatusEnum::diditTerminalStatuses(), true)) {
- $attributes['completed_at'] = now();
- }
- $verification->fill($attributes)->save();
- return $verification;
- }
- private function applyDecision(
- User $user,
- IdentityVerification $verification,
- string $status,
- array $decision,
- ): void {
- $profile = $this->profileFor($user);
- if (! $profile) {
- return;
- }
- if ($status === 'Approved' && $this->passesAllGates($verification, $decision)) {
- $this->approve($user, $profile, $verification);
- return;
- }
- if ($status === 'Declined') {
- $this->decline($profile);
- return;
- }
- // Approved com pendencia, In Review ou Abandoned: decisao humana.
- $this->sendToReview($profile);
- }
- private function passesAllGates(IdentityVerification $verification, array $decision): bool
- {
- $required = ['id_verifications', 'liveness_checks', 'face_matches'];
- foreach ($required as $key) {
- $items = $this->extractor->items($decision, $key);
- if ($items === []) {
- $this->logGate($verification, "feature ausente: {$key}");
- return false;
- }
- foreach ($items as $item) {
- if (data_get($item, 'status') !== 'Approved') {
- $this->logGate($verification, "feature reprovada: {$key}");
- return false;
- }
- }
- }
- $minLiveness = (float) config('services.didit.liveness_min_score');
- $minFaceMatch = (float) config('services.didit.face_match_min_score');
- if ($verification->liveness_score === null || $verification->liveness_score < $minLiveness) {
- $this->logGate($verification, 'liveness abaixo do minimo');
- return false;
- }
- if ($verification->face_match_score === null || $verification->face_match_score < $minFaceMatch) {
- $this->logGate($verification, 'face match abaixo do minimo');
- return false;
- }
- if ($this->extractor->actionableWarnings($decision) !== []) {
- $this->logGate($verification, 'warnings acionaveis presentes');
- return false;
- }
- return true;
- }
- private function approve(User $user, Model $profile, IdentityVerification $verification): void
- {
- DB::transaction(function () use ($profile, $verification) {
- $profile->forceFill([
- 'identity_verification_status' => IdentityVerificationStatusEnum::APPROVED->value,
- 'identity_verified_at' => now(),
- ]);
- if ($profile instanceof Provider) {
- $profile->document_verified = true;
- }
- if ($profile->profile_media_id) {
- $profile->selfie_verified = true;
- }
- $profile->save();
- $verification->forceFill(['completed_at' => now()])->save();
- });
- if ($profile instanceof Provider) {
- $this->providerService->approve($profile->id);
- }
- Log::channel('didit')->info('Verificacao aprovada automaticamente', [
- 'user_id' => $user->id,
- 'session_id' => $verification->session_id,
- ]);
- }
- private function decline(Model $profile): void
- {
- $attempts = (int) $profile->identity_verification_attempts + 1;
- $max = (int) config('services.didit.max_attempts', 3);
- $profile->forceFill([
- 'identity_verification_attempts' => $attempts,
- 'identity_verification_status' => $attempts >= $max
- ? IdentityVerificationStatusEnum::IN_REVIEW->value
- : IdentityVerificationStatusEnum::DECLINED->value,
- ])->save();
- }
- private function sendToReview(Model $profile): void
- {
- $profile->forceFill([
- 'identity_verification_status' => IdentityVerificationStatusEnum::IN_REVIEW->value,
- ])->save();
- }
- private function resolveUser(array $payload): ?User
- {
- $vendorData = data_get($payload, 'vendor_data');
- if (is_numeric($vendorData)) {
- return User::find((int) $vendorData);
- }
- $sessionId = data_get($payload, 'session_id');
- return IdentityVerification::where('session_id', $sessionId)->first()?->user;
- }
- private function profileFor(User $user): Provider|Client|null
- {
- return $user->type === UserTypeEnum::PROVIDER
- ? $user->provider
- : $user->client;
- }
- private function sanitize(array $decision): array
- {
- array_walk_recursive($decision, static function (&$value) {
- if (is_string($value) && str_starts_with($value, 'http')) {
- $value = explode('?', $value)[0];
- }
- });
- return $decision;
- }
- private function logGate(IdentityVerification $verification, string $reason): void
- {
- Log::channel('didit')->info('Aprovacao automatica barrada', [
- 'session_id' => $verification->session_id,
- 'motivo' => $reason,
- ]);
- }
- }
|