ProcessAsaasWebhookJob.php 7.9 KB

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