WebhookService.php 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. <?php
  2. namespace App\Services;
  3. use App\Enums\PaymentStatusEnum;
  4. use App\Models\Payment;
  5. use App\Models\Webhook;
  6. use App\Services\Pagarme\PagarmeAnticipationService;
  7. use App\Services\Pagarme\PagarmePaymentService;
  8. use Illuminate\Support\Facades\Log;
  9. class WebhookService
  10. {
  11. private const PAYMENT_EVENTS = [
  12. 'order.paid',
  13. 'order.payment_failed',
  14. 'order.canceled',
  15. 'order.created',
  16. 'order.closed',
  17. 'order.updated',
  18. 'charge.created',
  19. 'charge.paid',
  20. 'charge.payment_failed',
  21. 'charge.refunded',
  22. 'charge.pending',
  23. 'charge.processing',
  24. 'charge.underpaid',
  25. 'charge.overpaid',
  26. 'charge.partial_canceled',
  27. 'charge.chargedback',
  28. 'charge.updated',
  29. ];
  30. public function __construct(
  31. private readonly PagarmePaymentService $pagarmePaymentService,
  32. private readonly PagarmeAnticipationService $pagarmeAnticipationService,
  33. private readonly PaymentService $paymentService,
  34. private readonly ProviderWithdrawalService $providerWithdrawalService,
  35. ) {
  36. }
  37. public function handlePagarme(array $payload): ?Payment
  38. {
  39. $event = $payload['type'] ?? null;
  40. $data = $payload['data'] ?? [];
  41. $data = is_array($data) ? $data : [];
  42. $webhook = $this->recordWebhook($payload, $event, $data);
  43. Log::channel('pagarme')->info('Pagar.me webhook received', [
  44. 'hook_id' => $payload['id'] ?? null,
  45. 'event' => $event,
  46. ]);
  47. if (! in_array($event, self::PAYMENT_EVENTS, true)) {
  48. if (str_starts_with((string) $event, 'transfer.')) {
  49. $this->providerWithdrawalService->handleTransferWebhook($data);
  50. $webhook->update([
  51. 'status' => 'processed',
  52. 'processed_at' => now(),
  53. ]);
  54. return null;
  55. }
  56. $webhook->update([
  57. 'status' => 'ignored',
  58. 'processed_at' => now(),
  59. ]);
  60. return null;
  61. }
  62. $orderResponse = $this->normalizeOrderResponse($event, $data);
  63. $payment = $this->findPayment($orderResponse);
  64. if (! $payment) {
  65. $webhook->update([
  66. 'status' => 'payment_not_found',
  67. 'processed_at' => now(),
  68. ]);
  69. Log::channel('pagarme')->warning('Payment not found for Pagar.me webhook', [
  70. 'hook_id' => $payload['id'] ?? null,
  71. 'event' => $event,
  72. 'order_id' => $orderResponse['id'] ?? null,
  73. 'charge_id' => $orderResponse['charges'][0]['id'] ?? null,
  74. 'transaction_id' => $orderResponse['charges'][0]['last_transaction']['id'] ?? null,
  75. 'metadata' => $orderResponse['metadata'] ?? [],
  76. ]);
  77. return null;
  78. }
  79. try {
  80. $payment = $this->pagarmePaymentService->applyGatewayResponseToPayment($payment, $orderResponse);
  81. $this->paymentService->syncScheduleStatusAfterPayment($payment->schedule, $payment);
  82. if ($this->shouldCreateCreditCardAnticipation($event, $payment)) {
  83. $anticipationLimits = $this->pagarmeAnticipationService->fetchAnticipationLimitsForPayment($payment);
  84. $this->pagarmeAnticipationService->createBulkAnticipation($payment, $anticipationLimits);
  85. }
  86. $webhook->update([
  87. 'payment_id' => $payment->id,
  88. 'status' => 'processed',
  89. 'processed_at' => now(),
  90. ]);
  91. return $payment;
  92. } catch (\Throwable $e) {
  93. $webhook->update([
  94. 'payment_id' => $payment->id,
  95. 'status' => 'failed',
  96. 'processed_at' => now(),
  97. 'error_message' => $e->getMessage(),
  98. ]);
  99. throw $e;
  100. }
  101. }
  102. //
  103. private function recordWebhook(array $payload, ?string $event, array $data): Webhook
  104. {
  105. $references = $this->extractReferences($event, $data);
  106. $hookId = $payload['id'] ?? null;
  107. $attributes = [
  108. 'hook_id' => $hookId,
  109. 'provider' => 'pagarme',
  110. 'event' => $event,
  111. 'account_id' => $payload['account']['id'] ?? null,
  112. 'order_id' => $references['order_id'],
  113. 'charge_id' => $references['charge_id'],
  114. 'transaction_id' => $references['transaction_id'],
  115. 'status' => 'received',
  116. 'payload' => $payload,
  117. 'received_at' => now(),
  118. 'processed_at' => null,
  119. 'error_message' => null,
  120. ];
  121. if (empty($hookId)) {
  122. return Webhook::create($attributes);
  123. }
  124. $webhook = Webhook::firstOrNew([
  125. 'provider' => 'pagarme',
  126. 'hook_id' => $hookId,
  127. ]);
  128. $webhook->fill([
  129. ...$attributes,
  130. 'attempts_count' => $webhook->exists ? $webhook->attempts_count + 1 : 1,
  131. ])->save();
  132. return $webhook;
  133. }
  134. private function extractReferences(?string $event, array $data): array
  135. {
  136. if (str_starts_with((string) $event, 'order.')) {
  137. $charge = $data['charges'][0] ?? [];
  138. return [
  139. 'order_id' => $data['id'] ?? null,
  140. 'charge_id' => $charge['id'] ?? null,
  141. 'transaction_id' => $charge['last_transaction']['id'] ?? null,
  142. ];
  143. }
  144. if (str_starts_with((string) $event, 'charge.')) {
  145. return [
  146. 'order_id' => $data['order']['id'] ?? $data['order_id'] ?? null,
  147. 'charge_id' => $data['id'] ?? null,
  148. 'transaction_id' => $data['last_transaction']['id'] ?? null,
  149. ];
  150. }
  151. return [
  152. 'order_id' => null,
  153. 'charge_id' => null,
  154. 'transaction_id' => null,
  155. ];
  156. }
  157. private function normalizeOrderResponse(?string $event, array $data): array
  158. {
  159. if (str_starts_with((string) $event, 'order.')) {
  160. return $data;
  161. }
  162. return [
  163. 'id' => $data['order']['id'] ?? $data['order_id'] ?? null,
  164. 'metadata' => $data['metadata'] ?? $data['order']['metadata'] ?? [],
  165. 'charges' => [$data],
  166. ];
  167. }
  168. private function findPayment(array $orderResponse): ?Payment
  169. {
  170. $charge = $orderResponse['charges'][0] ?? [];
  171. $transaction = $charge['last_transaction'] ?? [];
  172. $metadata = $orderResponse['metadata'] ?? $charge['metadata'] ?? [];
  173. $metadataId = filter_var($metadata['payment_id'] ?? null, FILTER_VALIDATE_INT) ?: null;
  174. $chargeId = $charge['id'] ?? null;
  175. $transactionId = $transaction['id'] ?? null;
  176. $orderId = $orderResponse['id'] ?? null;
  177. $references = array_filter([$metadataId, $chargeId, $transactionId, $orderId]);
  178. if (empty($references)) {
  179. return null;
  180. }
  181. return Payment::query()
  182. ->where('gateway_provider', 'pagarme')
  183. ->where(function ($query) use ($metadataId, $chargeId, $transactionId, $orderId) {
  184. $query
  185. ->when($metadataId, fn ($query) => $query->orWhere('id', $metadataId))
  186. ->when($chargeId, fn ($query) => $query->orWhere('gateway_entity_reference', $chargeId))
  187. ->when($transactionId, fn ($query) => $query->orWhere('gateway_operation_reference', $transactionId))
  188. ->when($orderId, fn ($query) => $query->orWhere(function ($query) use ($orderId) {
  189. $query->where('gateway_entity_label', 'order')
  190. ->where('gateway_entity_reference', $orderId);
  191. }));
  192. })
  193. ->latest('id')
  194. ->first();
  195. }
  196. private function shouldCreateCreditCardAnticipation(?string $event, Payment $payment): bool
  197. {
  198. return $event === 'charge.paid'
  199. && $payment->payment_method === 'credit_card'
  200. && $payment->status === PaymentStatusEnum::PAID;
  201. }
  202. }