ProcessAsaasWebhookJob.php 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. <?php
  2. namespace App\Jobs;
  3. use App\Enums\ReceivableStatus;
  4. use App\Models\FranchiseeAccountReceive;
  5. use Illuminate\Bus\Queueable;
  6. use Illuminate\Contracts\Queue\ShouldQueue;
  7. use Illuminate\Foundation\Bus\Dispatchable;
  8. use Illuminate\Queue\InteractsWithQueue;
  9. use Illuminate\Queue\SerializesModels;
  10. use Illuminate\Support\Facades\Log;
  11. class ProcessAsaasWebhookJob implements ShouldQueue
  12. {
  13. use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
  14. private const EVENT_STATUS_MAP = [
  15. 'PAYMENT_RECEIVED' => ReceivableStatus::PAID,
  16. 'PAYMENT_CONFIRMED' => ReceivableStatus::PAID,
  17. 'PAYMENT_OVERDUE' => ReceivableStatus::OVERDUE,
  18. 'PAYMENT_DELETED' => ReceivableStatus::CANCELLED,
  19. 'PAYMENT_REFUNDED' => ReceivableStatus::CANCELLED,
  20. ];
  21. public array $payload;
  22. public function __construct(array $payload)
  23. {
  24. $this->payload = $payload;
  25. }
  26. public function handle(): void
  27. {
  28. $event = $this->payload['event'] ?? null;
  29. $payment = $this->payload['payment'] ?? [];
  30. if (!$event || empty($payment)) {
  31. Log::warning('Asaas Webhook Job: payload incompleto', $this->payload);
  32. return;
  33. }
  34. $newStatus = self::EVENT_STATUS_MAP[$event] ?? null;
  35. if (!$newStatus) {
  36. Log::info("Asaas Webhook Job: evento '{$event}' ignorado (não mapeado).");
  37. return;
  38. }
  39. $externalReference = $payment['externalReference'] ?? null;
  40. $asaasId = $payment['id'] ?? null;
  41. // Cobrança de aluno (parcela) cobrada pela conta Asaas PRÓPRIA da unidade.
  42. if ($externalReference && str_starts_with($externalReference, 'STU_')) {
  43. $this->handleStudentInstallment($externalReference, $payment, $event, $newStatus);
  44. return;
  45. }
  46. $receive = null;
  47. $type = 'unknown';
  48. if ($externalReference) {
  49. // Recebível de Franquia (TBR) cobrado pela conta da Matriz.
  50. if (str_starts_with($externalReference, 'TBR_')) {
  51. $id = str_replace('TBR_', '', $externalReference);
  52. $receive = FranchiseeAccountReceive::find($id);
  53. $type = 'FranchiseeAccountReceive';
  54. }
  55. // Backward compatibility para faturas de teste geradas antes do prefixo
  56. elseif (is_numeric($externalReference)) {
  57. $receive = FranchiseeAccountReceive::find($externalReference);
  58. $type = 'FranchiseeAccountReceive (legacy)';
  59. }
  60. }
  61. // Fallback por asaas_id caso externalReference venha nulo
  62. if (!$receive && $asaasId) {
  63. $receive = FranchiseeAccountReceive::where('asaas_id', $asaasId)->first();
  64. if ($receive) {
  65. $type = 'FranchiseeAccountReceive (by asaas_id)';
  66. }
  67. }
  68. if (!$receive) {
  69. Log::warning("Asaas Webhook Job: registro não encontrado", [
  70. 'externalReference' => $externalReference,
  71. 'asaas_id' => $asaasId,
  72. 'event' => $event,
  73. ]);
  74. return;
  75. }
  76. // Comparação segura de status (a model pode expor enum ou string).
  77. $currentStatus = $receive->status instanceof ReceivableStatus ? $receive->status->value : $receive->status;
  78. $newStatusValue = $newStatus instanceof ReceivableStatus ? $newStatus->value : $newStatus;
  79. // Idempotência
  80. if ($currentStatus === ReceivableStatus::PAID->value && $newStatusValue === ReceivableStatus::PAID->value) {
  81. Log::info("Asaas Webhook Job: registro {$type} #{$receive->id} já está pago. Ignorando duplicata.");
  82. return;
  83. }
  84. $updateData = [
  85. 'status' => $newStatusValue,
  86. 'asaas_status' => $payment['status'] ?? $event,
  87. ];
  88. if ($newStatusValue === ReceivableStatus::PAID->value) {
  89. $updateData['payment_date'] = $payment['paymentDate'] ?? $payment['confirmedDate'] ?? now();
  90. $updateData['paid_value'] = $payment['value'] ?? $receive->value;
  91. }
  92. $receive->update($updateData);
  93. // Reflete a baixa na Conta a Pagar da unidade quando o TBR é confirmado
  94. // pago via Asaas (mantém o espelho em dia sem baixa manual). A baixa manual
  95. // de cada lado continua independente para os demais casos.
  96. if ($newStatusValue === ReceivableStatus::PAID->value) {
  97. $payable = \App\Models\UnitAccountPayable::where('franchisee_account_receive_id', $receive->id)
  98. ->whereNotIn('status', ['paid', 'cancelled'])
  99. ->first();
  100. if ($payable) {
  101. $payable->update([
  102. 'status' => 'paid',
  103. 'paid_value' => $payable->value,
  104. 'payment_date' => $updateData['payment_date'] ?? now(),
  105. ]);
  106. Log::info("Asaas Webhook Job: Conta a Pagar da unidade #{$payable->id} baixada automaticamente (TBR pago).");
  107. }
  108. }
  109. Log::info("Asaas Webhook Job: {$type} #{$receive->id} atualizado para '{$newStatusValue}'", [
  110. 'event' => $event,
  111. 'asaas_id' => $asaasId,
  112. ]);
  113. }
  114. /**
  115. * Baixa/atualiza a parcela do aluno cobrada pela conta Asaas da unidade.
  116. */
  117. private function handleStudentInstallment(string $externalReference, array $payment, string $event, ReceivableStatus $newStatus): void
  118. {
  119. $id = str_replace('STU_', '', $externalReference);
  120. $installment = \App\Models\StudentContractInstallment::find($id);
  121. if (!$installment) {
  122. Log::warning("Asaas Webhook Job: parcela {$externalReference} não encontrada.", [
  123. 'event' => $event,
  124. 'asaas_id' => $payment['id'] ?? null,
  125. ]);
  126. return;
  127. }
  128. // Mapeia o status do recebível para o vocabulário das parcelas.
  129. $statusMap = [
  130. ReceivableStatus::PAID->value => 'paid',
  131. ReceivableStatus::OVERDUE->value => 'overdue',
  132. ReceivableStatus::CANCELLED->value => 'cancelled',
  133. ];
  134. $installmentStatus = $statusMap[$newStatus->value] ?? $installment->status;
  135. // Idempotência: já pago e continua pago → ignora duplicata.
  136. if ($installment->status === 'paid' && $installmentStatus === 'paid') {
  137. Log::info("Asaas Webhook Job: parcela #{$installment->id} já paga. Ignorando duplicata.");
  138. return;
  139. }
  140. $update = [
  141. 'status' => $installmentStatus,
  142. 'asaas_status' => $payment['status'] ?? $event,
  143. ];
  144. if ($installmentStatus === 'paid') {
  145. $update['payment_date'] = $payment['paymentDate'] ?? $payment['confirmedDate'] ?? now();
  146. $update['paid_value'] = $payment['value'] ?? $installment->value;
  147. }
  148. $installment->update($update);
  149. Log::info("Asaas Webhook Job: parcela #{$installment->id} atualizada para '{$installmentStatus}'.", [
  150. 'event' => $event,
  151. 'asaas_id' => $payment['id'] ?? null,
  152. ]);
  153. }
  154. }