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, ]); } }