浏览代码

feat(unit-payment-account): dedupe de webhook quando várias unidades usam a mesma chave

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ebagabee 4 周之前
父节点
当前提交
7b21f1ca21
共有 1 个文件被更改,包括 52 次插入20 次删除
  1. 52 20
      app/Services/UnitPaymentAccountService.php

+ 52 - 20
app/Services/UnitPaymentAccountService.php

@@ -22,8 +22,10 @@ public function getByUnitId(int $unitId): ?UnitPaymentAccount
     /**
      * Cria/atualiza a conta de pagamento da unidade.
      *
-     * Ao informar uma chave: valida na Asaas e registra o webhook (síncrono e
-     * estrito — chave inválida bloqueia). Ao limpar: remove o webhook (best-effort).
+     * Ao informar uma chave: valida na Asaas e garante um webhook. Uma MESMA chave
+     * pode ser compartilhada por várias unidades (mesmo dono) — nesse caso o webhook
+     * é registrado uma única vez e reaproveitado (dedupe por hash da chave). As
+     * finanças seguem separadas por unidade (externalReference STU_/TBR_).
      */
     public function upsertAsaasApiKey(int $unitId, ?string $apiKey): UnitPaymentAccount
     {
@@ -33,17 +35,20 @@ public function upsertAsaasApiKey(int $unitId, ?string $apiKey): UnitPaymentAcco
         $model = UnitPaymentAccount::firstOrNew(['unit_id' => $unitId]);
 
         if (!$hasKey) {
-            $this->removeWebhook($model);
+            $this->releaseWebhook($model);
 
-            $model->asaas_api_key    = null;
-            $model->asaas_webhook_id = null;
-            $model->status           = UnitPaymentAccount::STATUS_PENDING;
-            $model->onboarded_at     = null;
+            $model->asaas_api_key      = null;
+            $model->asaas_api_key_hash = null;
+            $model->asaas_webhook_id   = null;
+            $model->status             = UnitPaymentAccount::STATUS_PENDING;
+            $model->onboarded_at       = null;
             $model->save();
 
             return $model->fresh();
         }
 
+        $hash = hash('sha256', $apiKey);
+
         // Reenvio da mesma chave já configurada: mantém como está.
         if ($model->exists && $model->asaas_api_key === $apiKey && $model->asaas_webhook_id) {
             return $model->fresh();
@@ -57,36 +62,63 @@ public function upsertAsaasApiKey(int $unitId, ?string $apiKey): UnitPaymentAcco
             ]);
         }
 
-        // Troca de chave: remove o webhook antigo antes de registrar o novo.
-        $this->removeWebhook($model);
+        // Troca de chave: libera o webhook antigo (só remove se ninguém mais usa).
+        $this->releaseWebhook($model);
+
+        $webhookId = $this->resolveWebhookId($unitId, $apiKey, $hash);
+
+        $model->asaas_api_key      = $apiKey;
+        $model->asaas_api_key_hash = $hash;
+        $model->asaas_webhook_id   = $webhookId;
+        $model->status             = UnitPaymentAccount::STATUS_ACTIVE;
+        $model->onboarded_at       = $model->onboarded_at ?? now();
+        $model->save();
+
+        return $model->fresh();
+    }
+
+    /**
+     * Reaproveita o webhook de outra unidade que usa a MESMA chave; senão, registra.
+     */
+    private function resolveWebhookId(int $unitId, string $apiKey, string $hash): string
+    {
+        $sibling = UnitPaymentAccount::where('asaas_api_key_hash', $hash)
+            ->where('unit_id', '!=', $unitId)
+            ->whereNotNull('asaas_webhook_id')
+            ->first();
+
+        if ($sibling) {
+            return $sibling->asaas_webhook_id;
+        }
 
         try {
-            $webhookId = $this->webhookService->register($apiKey);
+            return $this->webhookService->register($apiKey);
         } catch (AsaasException $e) {
             Log::error("Falha ao registrar webhook Asaas da unidade {$unitId}: " . $e->getMessage());
             throw ValidationException::withMessages([
                 'asaas_api_key' => 'Não foi possível registrar o webhook na Asaas. Tente novamente.',
             ]);
         }
-
-        $model->asaas_api_key    = $apiKey;
-        $model->asaas_webhook_id = $webhookId;
-        $model->status           = UnitPaymentAccount::STATUS_ACTIVE;
-        $model->onboarded_at     = $model->onboarded_at ?? now();
-        $model->save();
-
-        return $model->fresh();
     }
 
     /**
-     * Remove o webhook atual da conta (ignora erros — best-effort).
+     * Remove o webhook atual — a menos que outra unidade ainda o utilize (chave
+     * compartilhada). Best-effort: erros são apenas logados.
      */
-    private function removeWebhook(UnitPaymentAccount $model): void
+    private function releaseWebhook(UnitPaymentAccount $model): void
     {
         if (!$model->asaas_api_key || !$model->asaas_webhook_id) {
             return;
         }
 
+        $stillShared = UnitPaymentAccount::where('asaas_webhook_id', $model->asaas_webhook_id)
+            ->where('unit_id', '!=', $model->unit_id)
+            ->exists();
+
+        if ($stillShared) {
+            return;
+        }
+
         try {
             $this->webhookService->delete($model->asaas_api_key, $model->asaas_webhook_id);
         } catch (\Throwable $e) {