SyncDiditSessions.php 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. <?php
  2. namespace App\Commands;
  3. use App\Enums\IdentityVerificationStatusEnum;
  4. use App\Models\IdentityVerification;
  5. use App\Services\Didit\DiditDecisionService;
  6. use App\Services\Didit\DiditSessionService;
  7. use Illuminate\Console\Command;
  8. use Illuminate\Support\Facades\Log;
  9. use Throwable;
  10. /**
  11. * Reconcilia verificacoes cujo webhook nao chegou.
  12. *
  13. * O Didit tenta entregar 3 vezes (imediata, ~1min, ~4min) e desiste. Se a API
  14. * estiver fora do ar nessa janela, a verificacao fica presa em Not Started ou
  15. * In Progress para sempre. Este comando busca a decisao pela API e aplica o
  16. * mesmo caminho do webhook.
  17. */
  18. class SyncDiditSessions extends Command
  19. {
  20. protected $signature = 'didit:sync
  21. {session_id? : Sessao especifica a reconciliar}
  22. {--stale-hours=6 : Idade minima, em horas, para considerar uma verificacao travada}
  23. {--limit=50 : Maximo de verificacoes por execucao}
  24. {--dry-run : Apenas lista o que seria reconciliado}';
  25. protected $description = 'Reconcilia verificacoes de identidade cujo webhook do Didit nao chegou';
  26. public function __construct(
  27. private readonly DiditSessionService $sessionService,
  28. private readonly DiditDecisionService $decisionService,
  29. ) {
  30. parent::__construct();
  31. }
  32. public function handle(): int
  33. {
  34. $verifications = $this->targets();
  35. if ($verifications->isEmpty()) {
  36. $this->info('Nenhuma verificacao para reconciliar.');
  37. return self::SUCCESS;
  38. }
  39. $this->info("Verificacoes a reconciliar: {$verifications->count()}");
  40. if ($this->option('dry-run')) {
  41. $this->table(
  42. ['id', 'session_id', 'status', 'usuario', 'iniciada em'],
  43. $verifications->map(fn (IdentityVerification $v) => [
  44. $v->id, $v->session_id, $v->didit_status, $v->user_id, (string) $v->started_at,
  45. ])->all(),
  46. );
  47. return self::SUCCESS;
  48. }
  49. $reconciled = 0;
  50. $unchanged = 0;
  51. $failed = 0;
  52. foreach ($verifications as $verification) {
  53. try {
  54. $decision = $this->sessionService->getDecision($verification->session_id);
  55. $status = (string) data_get($decision, 'status');
  56. if ($status === '' || $status === $verification->didit_status) {
  57. $unchanged++;
  58. continue;
  59. }
  60. // Mesmo caminho do webhook: o payload real tambem traz o decision inteiro.
  61. $this->decisionService->handleWebhook([
  62. 'session_id' => $verification->session_id,
  63. 'webhook_type' => 'status.updated',
  64. 'status' => $status,
  65. 'vendor_data' => (string) $verification->user_id,
  66. 'workflow_id' => $verification->workflow_id,
  67. 'decision' => $decision,
  68. ]);
  69. $this->line(" #{$verification->id} {$verification->didit_status} -> {$status}");
  70. $reconciled++;
  71. } catch (Throwable $e) {
  72. $failed++;
  73. $this->error(" #{$verification->id}: {$e->getMessage()}");
  74. Log::channel('didit')->error('Falha ao reconciliar sessao', [
  75. 'session_id' => $verification->session_id,
  76. 'exception' => $e->getMessage(),
  77. ]);
  78. }
  79. }
  80. $this->newLine();
  81. $this->info("Reconciliadas: {$reconciled} | sem mudanca: {$unchanged} | falhas: {$failed}");
  82. return $failed > 0 ? self::FAILURE : self::SUCCESS;
  83. }
  84. //
  85. /** @return \Illuminate\Support\Collection<int, IdentityVerification> */
  86. private function targets()
  87. {
  88. $query = IdentityVerification::query();
  89. if ($sessionId = $this->argument('session_id')) {
  90. return $query->where('session_id', $sessionId)->get();
  91. }
  92. return $query
  93. ->whereIn('didit_status', ['Not Started', 'In Progress'])
  94. ->where('started_at', '<=', now()->subHours((int) $this->option('stale-hours')))
  95. ->orderBy('started_at')
  96. ->limit((int) $this->option('limit'))
  97. ->get();
  98. }
  99. }