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; $type = 'unknown'; if ($externalReference) { // Se for um recebível de Franquia if (str_starts_with($externalReference, 'TBR_')) { $id = str_replace('TBR_', '', $externalReference); $receive = FranchiseeAccountReceive::find($id); $type = 'FranchiseeAccountReceive'; } // Se for parcela de aluno elseif (str_starts_with($externalReference, 'STU_')) { $id = str_replace('STU_', '', $externalReference); $receive = StudentContractInstallment::find($id); $type = 'StudentContractInstallment'; } // Backward compatibility para faturas de teste geradas antes do prefixo elseif (is_numeric($externalReference)) { $receive = FranchiseeAccountReceive::find($externalReference); $type = 'FranchiseeAccountReceive (legacy)'; } } // Fallback por asaas_id caso externalReference venha nulo if (!$receive && $asaasId) { $receive = FranchiseeAccountReceive::where('asaas_id', $asaasId)->first(); if ($receive) { $type = 'FranchiseeAccountReceive (by asaas_id)'; } else { $receive = StudentContractInstallment::where('asaas_id', $asaasId)->first(); if ($receive) { $type = 'StudentContractInstallment (by asaas_id)'; } } } if (!$receive) { Log::warning("Asaas Webhook Job: registro não encontrado", [ 'externalReference' => $externalReference, 'asaas_id' => $asaasId, 'event' => $event, ]); return; } // Conversão de status: se a model usa string (como StudentContractInstallment) // Precisamos comparar de forma segura $currentStatus = $receive->status instanceof ReceivableStatus ? $receive->status->value : $receive->status; $newStatusValue = $newStatus instanceof ReceivableStatus ? $newStatus->value : $newStatus; // Idempotência if ($currentStatus === ReceivableStatus::PAID->value && $newStatusValue === ReceivableStatus::PAID->value) { Log::info("Asaas Webhook Job: registro {$type} #{$receive->id} já está pago. Ignorando duplicata."); return; } $updateData = [ 'status' => $newStatusValue, 'asaas_status' => $payment['status'] ?? $event, ]; if ($newStatusValue === ReceivableStatus::PAID->value) { $updateData['payment_date'] = $payment['paymentDate'] ?? $payment['confirmedDate'] ?? now(); // A tabela do Aluno usa 'paid_value', a tabela da Franquia também. $updateData['paid_value'] = $payment['value'] ?? $receive->value; } $receive->update($updateData); Log::info("Asaas Webhook Job: {$type} #{$receive->id} atualizado para '{$newStatusValue}'", [ 'event' => $event, 'asaas_id' => $asaasId, ]); } }