Переглянути джерело

feat: implement asynchronous webhook handler for Asaas payment notifications

ebagabee 1 тиждень тому
батько
коміт
87c8fbe94e

+ 44 - 0
app/Http/Controllers/Webhooks/AsaasWebhookController.php

@@ -0,0 +1,44 @@
+<?php
+
+namespace App\Http\Controllers\Webhooks;
+
+use App\Http\Controllers\Controller;
+use App\Jobs\ProcessAsaasWebhookJob;
+use Illuminate\Http\JsonResponse;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Log;
+
+class AsaasWebhookController extends Controller
+{
+    public function handle(Request $request): JsonResponse
+    {
+        $expectedToken = config('services.asaas.webhook_token');
+
+        if (empty($expectedToken)) {
+            Log::warning('Asaas Webhook: ASAAS_WEBHOOK_TOKEN não está configurado no .env');
+            return response()->json(['error' => 'Webhook não configurado'], 500);
+        }
+
+        $receivedToken = $request->header('asaas-access-token');
+
+        if ($receivedToken !== $expectedToken) {
+            Log::warning('Asaas Webhook: token inválido recebido', [
+                'received' => substr($receivedToken ?? '', 0, 10) . '...',
+                'ip' => $request->ip(),
+            ]);
+
+            return response()->json(['error' => 'Unauthorized'], 401);
+        }
+
+        $payload = $request->all();
+
+        Log::info('Asaas Webhook recebido', [
+            'event' => $payload['event'] ?? 'unknown',
+            'payment_id' => $payload['payment']['id'] ?? null,
+        ]);
+
+        ProcessAsaasWebhookJob::dispatch($payload);
+
+        return response()->json(['status' => 'received'], 200);
+    }
+}

+ 97 - 0
app/Jobs/ProcessAsaasWebhookJob.php

@@ -0,0 +1,97 @@
+<?php
+
+namespace App\Jobs;
+
+use App\Enums\ReceivableStatus;
+use App\Models\FranchiseeAccountReceive;
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Queue\SerializesModels;
+use Illuminate\Support\Facades\Log;
+
+class ProcessAsaasWebhookJob implements ShouldQueue
+{
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    private const EVENT_STATUS_MAP = [
+        'PAYMENT_RECEIVED'  => ReceivableStatus::PAID,
+        'PAYMENT_CONFIRMED' => ReceivableStatus::PAID,
+        'PAYMENT_OVERDUE'   => ReceivableStatus::OVERDUE,
+        'PAYMENT_DELETED'   => ReceivableStatus::CANCELLED,
+        'PAYMENT_REFUNDED'  => ReceivableStatus::CANCELLED,
+    ];
+
+    public array $payload;
+
+    public function __construct(array $payload)
+    {
+        $this->payload = $payload;
+    }
+
+    public function handle(): void
+    {
+        $event   = $this->payload['event'] ?? null;
+        $payment = $this->payload['payment'] ?? [];
+
+        if (!$event || empty($payment)) {
+            Log::warning('Asaas Webhook Job: payload incompleto', $this->payload);
+            return;
+        }
+
+        $newStatus = self::EVENT_STATUS_MAP[$event] ?? null;
+
+        if (!$newStatus) {
+            Log::info("Asaas Webhook Job: evento '{$event}' ignorado (não mapeado).");
+            return;
+        }
+
+        $externalReference = $payment['externalReference'] ?? null;
+        $asaasId           = $payment['id'] ?? null;
+
+        $receive = null;
+
+        if ($externalReference) {
+            $receive = FranchiseeAccountReceive::find($externalReference);
+        }
+
+        if (!$receive && $asaasId) {
+            $receive = FranchiseeAccountReceive::where('asaas_id', $asaasId)->first();
+        }
+
+        if (!$receive) {
+            Log::warning("Asaas Webhook Job: recebível não encontrado", [
+                'externalReference' => $externalReference,
+                'asaas_id' => $asaasId,
+                'event' => $event,
+            ]);
+            return;
+        }
+
+        // Se já está pago, não processa de novo
+        if ($receive->status === ReceivableStatus::PAID && $newStatus === ReceivableStatus::PAID) {
+            Log::info("Asaas Webhook Job: recebível #{$receive->id} já está pago. Ignorando duplicata.");
+            return;
+        }
+
+        // Atualizar o status
+        $updateData = [
+            'status'      => $newStatus,
+            'asaas_status' => $payment['status'] ?? $event,
+        ];
+
+        // Se foi pago, registrar a data de pagamento e o valor pago
+        if ($newStatus === ReceivableStatus::PAID) {
+            $updateData['payment_date'] = $payment['paymentDate'] ?? $payment['confirmedDate'] ?? now();
+            $updateData['paid_value']   = $payment['value'] ?? $receive->value;
+        }
+
+        $receive->update($updateData);
+
+        Log::info("Asaas Webhook Job: recebível #{$receive->id} atualizado para '{$newStatus->value}'", [
+            'event' => $event,
+            'asaas_id' => $asaasId,
+        ]);
+    }
+}

+ 6 - 0
routes/noAuthRoutes/asaas_webhook.php

@@ -0,0 +1,6 @@
+<?php
+
+use Illuminate\Support\Facades\Route;
+use App\Http\Controllers\Webhooks\AsaasWebhookController;
+
+Route::post('/webhooks/asaas', [AsaasWebhookController::class, 'handle']);