WebhookService.php 7.4 KB

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