Просмотр исходного кода

fix: idempotência e retry do CreateAsaasSubaccountJob

Evita colisão "email/CNPJ já em uso" causada por execuções concorrentes
e por retentar erro permanente do Asaas:
- WithoutOverlapping(unitId) serializa jobs da mesma unidade (POST /accounts
  do Asaas não é idempotente);
- erros 4xx do Asaas falham na hora (fail) sem consumir as 3 tentativas;
- 5xx/transitório continua com retry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ebagabee 1 месяц назад
Родитель
Сommit
60dda4c185
1 измененных файлов с 29 добавлено и 2 удалено
  1. 29 2
      app/Jobs/CreateAsaasSubaccountJob.php

+ 29 - 2
app/Jobs/CreateAsaasSubaccountJob.php

@@ -2,6 +2,7 @@
 
 
 namespace App\Jobs;
 namespace App\Jobs;
 
 
+use App\Exceptions\AsaasException;
 use App\Models\Unit;
 use App\Models\Unit;
 use App\Services\Integrations\Asaas\AsaasAccountService;
 use App\Services\Integrations\Asaas\AsaasAccountService;
 use Exception;
 use Exception;
@@ -9,6 +10,7 @@
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Foundation\Bus\Dispatchable;
 use Illuminate\Foundation\Bus\Dispatchable;
 use Illuminate\Queue\InteractsWithQueue;
 use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Queue\Middleware\WithoutOverlapping;
 use Illuminate\Queue\SerializesModels;
 use Illuminate\Queue\SerializesModels;
 use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Facades\Log;
 
 
@@ -24,6 +26,15 @@ public function __construct(int $unitId)
         $this->unitId = $unitId;
         $this->unitId = $unitId;
     }
     }
 
 
+    /**
+     * Serializa os jobs por unidade: evita que duas execuções concorrentes
+     * criem a subconta ao mesmo tempo (o POST /accounts do Asaas não é idempotente).
+     */
+    public function middleware(): array
+    {
+        return [(new WithoutOverlapping($this->unitId))->releaseAfter(10)->expireAfter(60)];
+    }
+
     public function handle(AsaasAccountService $accountService): void
     public function handle(AsaasAccountService $accountService): void
     {
     {
         $unit = Unit::find($this->unitId);
         $unit = Unit::find($this->unitId);
@@ -36,11 +47,27 @@ public function handle(AsaasAccountService $accountService): void
         try {
         try {
             $accountService->ensureSubaccount($unit);
             $accountService->ensureSubaccount($unit);
             Log::info("CreateAsaasSubaccountJob: Subconta criada com sucesso para a Unidade {$unit->id}.");
             Log::info("CreateAsaasSubaccountJob: Subconta criada com sucesso para a Unidade {$unit->id}.");
+        } catch (AsaasException $e) {
+            // Erros 4xx (ex.: e-mail/CNPJ já em uso) são permanentes: retentar só
+            // recriaria a colisão. Falha imediatamente, sem consumir as outras tentativas.
+            if ($e->getCode() >= 400 && $e->getCode() < 500) {
+                Log::error("CreateAsaasSubaccountJob: Erro permanente do Asaas para Unidade {$unit->id} — sem retry.", [
+                    'error' => $e->getMessage(),
+                ]);
+                $this->fail($e);
+                return;
+            }
+
+            // 5xx / instabilidade: deixa o mecanismo de retry agir.
+            Log::error("CreateAsaasSubaccountJob: Falha transitória para Unidade {$unit->id}.", [
+                'error' => $e->getMessage(),
+            ]);
+            throw $e;
         } catch (Exception $e) {
         } catch (Exception $e) {
             Log::error("CreateAsaasSubaccountJob: Falha ao criar subconta para Unidade {$unit->id}.", [
             Log::error("CreateAsaasSubaccountJob: Falha ao criar subconta para Unidade {$unit->id}.", [
-                'error' => $e->getMessage()
+                'error' => $e->getMessage(),
             ]);
             ]);
-            throw $e; // Throw to retry if needed
+            throw $e;
         }
         }
     }
     }
 }
 }