IdentityVerificationService.php 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. <?php
  2. namespace App\Services;
  3. use App\Enums\IdentityVerificationStatusEnum;
  4. use App\Enums\UserTypeEnum;
  5. use App\Http\Resources\IdentityVerificationResource;
  6. use App\Models\Client;
  7. use App\Models\IdentityVerification;
  8. use App\Models\Provider;
  9. use App\Models\User;
  10. use App\Services\Didit\DiditSessionService;
  11. use Illuminate\Database\Eloquent\Model;
  12. use Illuminate\Support\Facades\DB;
  13. use RuntimeException;
  14. class IdentityVerificationService
  15. {
  16. public function __construct(
  17. private readonly DiditSessionService $sessionService,
  18. private readonly ProviderService $providerService,
  19. ) {}
  20. public function startFor(User $user, bool $native = false): array
  21. {
  22. $verification = $this->sessionService->createSession($user, $native);
  23. return [
  24. 'session_id' => $verification->session_id,
  25. 'verification_url' => $this->sessionService->verificationUrl($verification),
  26. 'status' => $verification->didit_status,
  27. 'attempt' => $verification->attempt,
  28. 'attempts_left' => $this->attemptsLeft($this->profileFor($user)),
  29. ];
  30. }
  31. public function statusFor(User $user): array
  32. {
  33. $profile = $this->profileFor($user);
  34. $verification = $user->latestIdentityVerification()->first();
  35. return [
  36. 'identity_verification_status' => $profile?->identity_verification_status?->value
  37. ?? IdentityVerificationStatusEnum::NOT_STARTED->value,
  38. 'identity_verified_at' => $profile?->identity_verified_at,
  39. 'required' => (bool) ($profile?->identity_verification_required ?? false),
  40. 'attempts_left' => $this->attemptsLeft($profile),
  41. 'can_retry' => $this->canRetry($profile),
  42. 'has_open_session' => $verification !== null
  43. && in_array($verification->didit_status, ['Not Started', 'In Progress'], true),
  44. 'last_verification' => $verification
  45. ? new IdentityVerificationResource($verification)
  46. : null,
  47. ];
  48. }
  49. public function pending(int $perPage, int $page): array
  50. {
  51. $paginator = IdentityVerification::query()
  52. ->with('user')
  53. ->whereIn('didit_status', ['In Review', 'Declined'])
  54. ->orderBy('completed_at')
  55. ->paginate(perPage: $perPage, page: $page);
  56. return [
  57. 'data' => IdentityVerificationResource::collection($paginator->items()),
  58. 'total' => $paginator->total(),
  59. 'from' => $paginator->firstItem(),
  60. 'to' => $paginator->lastItem(),
  61. ];
  62. }
  63. /**
  64. * Busca o laudo com as imagens frescas: as URLs do Didit sao presignadas e de
  65. * vida curta, por isso nao sao persistidas e sim buscadas na hora de exibir.
  66. */
  67. public function findWithFreshDecision(int $id): ?array
  68. {
  69. $verification = IdentityVerification::with('user')->find($id);
  70. if (! $verification) {
  71. return null;
  72. }
  73. $decision = null;
  74. try {
  75. $decision = $this->sessionService->getDecision($verification->session_id);
  76. } catch (\Throwable) {
  77. // Laudo persistido ja basta para a decisao; so as imagens ficam indisponiveis.
  78. }
  79. return [
  80. 'verification' => new IdentityVerificationResource($verification),
  81. 'decision' => $decision ?? $verification->decision,
  82. 'decision_fresh' => $decision !== null,
  83. ];
  84. }
  85. public function review(int $id, bool $approved, User $reviewer, ?string $comment): IdentityVerification
  86. {
  87. $verification = IdentityVerification::with('user')->find($id);
  88. if (! $verification) {
  89. throw new RuntimeException(__('identity_verification.not_found'));
  90. }
  91. $user = $verification->user;
  92. $profile = $user ? $this->profileFor($user) : null;
  93. if (! $profile) {
  94. throw new RuntimeException(__('identity_verification.profile_missing'));
  95. }
  96. DB::transaction(function () use ($verification, $profile, $approved, $reviewer, $comment) {
  97. $verification->forceFill([
  98. 'reviewed_by' => $reviewer->id,
  99. 'reviewed_at' => now(),
  100. 'review_comment' => $comment,
  101. ])->save();
  102. $profile->forceFill([
  103. 'identity_verification_status' => $approved
  104. ? IdentityVerificationStatusEnum::APPROVED->value
  105. : IdentityVerificationStatusEnum::DECLINED->value,
  106. 'identity_verified_at' => $approved ? now() : null,
  107. ]);
  108. if ($approved && $profile instanceof Provider) {
  109. $profile->document_verified = true;
  110. }
  111. $profile->save();
  112. });
  113. if ($approved && $profile instanceof Provider) {
  114. $this->providerService->approve($profile->id);
  115. }
  116. return $verification->refresh();
  117. }
  118. //
  119. private function attemptsLeft(?Model $profile): int
  120. {
  121. $max = (int) config('services.didit.max_attempts', 3);
  122. return max(0, $max - (int) ($profile?->identity_verification_attempts ?? 0));
  123. }
  124. private function canRetry(?Model $profile): bool
  125. {
  126. if (! $profile) {
  127. return false;
  128. }
  129. $status = $profile->identity_verification_status;
  130. return $status instanceof IdentityVerificationStatusEnum
  131. && $status->allowsNewAttempt()
  132. && $this->attemptsLeft($profile) > 0;
  133. }
  134. private function profileFor(User $user): Provider|Client|null
  135. {
  136. return $user->type === UserTypeEnum::PROVIDER
  137. ? $user->provider
  138. : $user->client;
  139. }
  140. }