| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125 |
- <?php
- namespace App\Commands;
- use App\Enums\IdentityVerificationStatusEnum;
- use App\Models\IdentityVerification;
- use App\Services\Didit\DiditDecisionService;
- use App\Services\Didit\DiditSessionService;
- use Illuminate\Console\Command;
- use Illuminate\Support\Facades\Log;
- use Throwable;
- /**
- * Reconcilia verificacoes cujo webhook nao chegou.
- *
- * O Didit tenta entregar 3 vezes (imediata, ~1min, ~4min) e desiste. Se a API
- * estiver fora do ar nessa janela, a verificacao fica presa em Not Started ou
- * In Progress para sempre. Este comando busca a decisao pela API e aplica o
- * mesmo caminho do webhook.
- */
- class SyncDiditSessions extends Command
- {
- protected $signature = 'didit:sync
- {session_id? : Sessao especifica a reconciliar}
- {--stale-hours=6 : Idade minima, em horas, para considerar uma verificacao travada}
- {--limit=50 : Maximo de verificacoes por execucao}
- {--dry-run : Apenas lista o que seria reconciliado}';
- protected $description = 'Reconcilia verificacoes de identidade cujo webhook do Didit nao chegou';
- public function __construct(
- private readonly DiditSessionService $sessionService,
- private readonly DiditDecisionService $decisionService,
- ) {
- parent::__construct();
- }
- public function handle(): int
- {
- $verifications = $this->targets();
- if ($verifications->isEmpty()) {
- $this->info('Nenhuma verificacao para reconciliar.');
- return self::SUCCESS;
- }
- $this->info("Verificacoes a reconciliar: {$verifications->count()}");
- if ($this->option('dry-run')) {
- $this->table(
- ['id', 'session_id', 'status', 'usuario', 'iniciada em'],
- $verifications->map(fn (IdentityVerification $v) => [
- $v->id, $v->session_id, $v->didit_status, $v->user_id, (string) $v->started_at,
- ])->all(),
- );
- return self::SUCCESS;
- }
- $reconciled = 0;
- $unchanged = 0;
- $failed = 0;
- foreach ($verifications as $verification) {
- try {
- $decision = $this->sessionService->getDecision($verification->session_id);
- $status = (string) data_get($decision, 'status');
- if ($status === '' || $status === $verification->didit_status) {
- $unchanged++;
- continue;
- }
- // Mesmo caminho do webhook: o payload real tambem traz o decision inteiro.
- $this->decisionService->handleWebhook([
- 'session_id' => $verification->session_id,
- 'webhook_type' => 'status.updated',
- 'status' => $status,
- 'vendor_data' => (string) $verification->user_id,
- 'workflow_id' => $verification->workflow_id,
- 'decision' => $decision,
- ]);
- $this->line(" #{$verification->id} {$verification->didit_status} -> {$status}");
- $reconciled++;
- } catch (Throwable $e) {
- $failed++;
- $this->error(" #{$verification->id}: {$e->getMessage()}");
- Log::channel('didit')->error('Falha ao reconciliar sessao', [
- 'session_id' => $verification->session_id,
- 'exception' => $e->getMessage(),
- ]);
- }
- }
- $this->newLine();
- $this->info("Reconciliadas: {$reconciled} | sem mudanca: {$unchanged} | falhas: {$failed}");
- return $failed > 0 ? self::FAILURE : self::SUCCESS;
- }
- //
- /** @return \Illuminate\Support\Collection<int, IdentityVerification> */
- private function targets()
- {
- $query = IdentityVerification::query();
- if ($sessionId = $this->argument('session_id')) {
- return $query->where('session_id', $sessionId)->get();
- }
- return $query
- ->whereIn('didit_status', ['Not Started', 'In Progress'])
- ->where('started_at', '<=', now()->subHours((int) $this->option('stale-hours')))
- ->orderBy('started_at')
- ->limit((int) $this->option('limit'))
- ->get();
- }
- }
|