WebhookService.php 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. <?php
  2. namespace App\Services;
  3. use App\Models\Payment;
  4. use App\Models\Webhook;
  5. use App\Services\Didit\DiditDecisionService;
  6. use App\Services\Pagarme\PagarmePaymentService;
  7. use Illuminate\Support\Facades\Log;
  8. use Throwable;
  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 PaymentService $paymentService,
  33. private readonly ProviderWithdrawalService $providerWithdrawalService,
  34. ) {
  35. }
  36. public function handleDidit(array $payload): void
  37. {
  38. $hookId = data_get($payload, 'event_id') ?: sha1(implode('|', [
  39. data_get($payload, 'session_id'),
  40. data_get($payload, 'status'),
  41. data_get($payload, 'webhook_type'),
  42. ]));
  43. $webhook = Webhook::query()->firstOrNew([
  44. 'provider' => 'didit',
  45. 'hook_id' => $hookId,
  46. ]);
  47. if ($webhook->exists) {
  48. $webhook->increment('attempts_count');
  49. Log::channel('didit')->info('Webhook duplicado ignorado', ['hook_id' => $hookId]);
  50. return;
  51. }
  52. $webhook->fill([
  53. 'event' => data_get($payload, 'webhook_type'),
  54. 'status' => 'received',
  55. 'attempts_count' => 1,
  56. 'payload' => $payload,
  57. 'received_at' => now(),
  58. ])->save();
  59. try {
  60. $verification = app(DiditDecisionService::class)->handleWebhook($payload);
  61. $webhook->update([
  62. 'status' => $verification ? 'processed' : 'ignored',
  63. 'processed_at' => now(),
  64. ]);
  65. } catch (Throwable $e) {
  66. $webhook->update([
  67. 'status' => 'failed',
  68. 'processed_at' => now(),
  69. 'error_message' => $e->getMessage(),
  70. ]);
  71. Log::channel('didit')->error('Falha ao processar webhook do Didit', [
  72. 'hook_id' => $hookId,
  73. 'exception' => $e->getMessage(),
  74. ]);
  75. throw $e;
  76. }
  77. }
  78. public function handlePagarme(array $payload): ?Payment
  79. {
  80. $event = data_get($payload, 'type');
  81. $data = data_get($payload, 'data', []);
  82. $data = is_array($data) ? $data : [];
  83. $webhook = $this->recordWebhook($payload, $event, $data);
  84. Log::channel('pagarme')->info('Webhook do Pagar.me recebido', [
  85. 'hook_id' => data_get($payload, 'id'),
  86. 'event' => $event,
  87. ]);
  88. if (! in_array($event, self::PAYMENT_EVENTS, true)) {
  89. if (str_starts_with((string) $event, 'transfer.')) {
  90. $this->providerWithdrawalService->handleTransferWebhook($data);
  91. $webhook->update([
  92. 'status' => 'processed',
  93. 'processed_at' => now(),
  94. ]);
  95. return null;
  96. }
  97. $webhook->update([
  98. 'status' => 'ignored',
  99. 'processed_at' => now(),
  100. ]);
  101. return null;
  102. }
  103. $orderResponse = $this->normalizeOrderResponse($event, $data);
  104. $payment = $this->findPayment($orderResponse);
  105. if (! $payment) {
  106. $webhook->update([
  107. 'status' => 'payment_not_found',
  108. 'processed_at' => now(),
  109. ]);
  110. Log::channel('pagarme')->warning('Pagamento não encontrado para o webhook do Pagar.me', [
  111. 'hook_id' => data_get($payload, 'id'),
  112. 'event' => $event,
  113. 'order_id' => data_get($orderResponse, 'id'),
  114. 'charge_id' => data_get($orderResponse, 'charges.0.id'),
  115. 'transaction_id' => data_get($orderResponse, 'charges.0.last_transaction.id'),
  116. 'metadata' => data_get($orderResponse, 'metadata', []),
  117. ]);
  118. return null;
  119. }
  120. try {
  121. $payment = $this->pagarmePaymentService->applyGatewayResponseToPayment($payment, $orderResponse);
  122. $this->paymentService->syncPaymentTargets($payment);
  123. $webhook->update([
  124. 'payment_id' => $payment->id,
  125. 'status' => 'processed',
  126. 'processed_at' => now(),
  127. ]);
  128. return $payment;
  129. } catch (\Throwable $e) {
  130. $webhook->update([
  131. 'payment_id' => $payment->id,
  132. 'status' => 'failed',
  133. 'processed_at' => now(),
  134. 'error_message' => $e->getMessage(),
  135. ]);
  136. throw $e;
  137. }
  138. }
  139. //
  140. private function recordWebhook(array $payload, ?string $event, array $data): Webhook
  141. {
  142. $references = $this->extractReferences($event, $data);
  143. $hookId = data_get($payload, 'id');
  144. $attributes = [
  145. 'hook_id' => $hookId,
  146. 'provider' => 'pagarme',
  147. 'event' => $event,
  148. 'account_id' => data_get($payload, 'account.id'),
  149. 'order_id' => data_get($references, 'order_id'),
  150. 'charge_id' => data_get($references, 'charge_id'),
  151. 'transaction_id' => data_get($references, 'transaction_id'),
  152. 'status' => 'received',
  153. 'payload' => $payload,
  154. 'received_at' => now(),
  155. 'processed_at' => null,
  156. 'error_message' => null,
  157. ];
  158. if (empty($hookId)) {
  159. return Webhook::create($attributes);
  160. }
  161. $webhook = Webhook::firstOrNew([
  162. 'provider' => 'pagarme',
  163. 'hook_id' => $hookId,
  164. ]);
  165. $webhook->fill([
  166. ...$attributes,
  167. 'attempts_count' => $webhook->exists ? $webhook->attempts_count + 1 : 1,
  168. ])->save();
  169. return $webhook;
  170. }
  171. //
  172. private function extractReferences(?string $event, array $data): array
  173. {
  174. if (str_starts_with((string) $event, 'order.')) {
  175. $charge = data_get($data, 'charges.0', []);
  176. return [
  177. 'order_id' => data_get($data, 'id'),
  178. 'charge_id' => data_get($charge, 'id'),
  179. 'transaction_id' => data_get($charge, 'last_transaction.id'),
  180. ];
  181. }
  182. if (str_starts_with((string) $event, 'charge.')) {
  183. return [
  184. 'order_id' => data_get($data, 'order.id', data_get($data, 'order_id')),
  185. 'charge_id' => data_get($data, 'id'),
  186. 'transaction_id' => data_get($data, 'last_transaction.id'),
  187. ];
  188. }
  189. return [
  190. 'order_id' => null,
  191. 'charge_id' => null,
  192. 'transaction_id' => null,
  193. ];
  194. }
  195. private function findPayment(array $orderResponse): ?Payment
  196. {
  197. $charge = data_get($orderResponse, 'charges.0', []);
  198. $transaction = data_get($charge, 'last_transaction', []);
  199. $metadata = data_get($orderResponse, 'metadata', data_get($charge, 'metadata', []));
  200. $metadataId = filter_var(data_get($metadata, 'payment_id'), FILTER_VALIDATE_INT) ?: null;
  201. $chargeId = data_get($charge, 'id');
  202. $transactionId = data_get($transaction, 'id');
  203. $orderId = data_get($orderResponse, 'id');
  204. $references = array_filter([$metadataId, $chargeId, $transactionId, $orderId]);
  205. if (empty($references)) {
  206. return null;
  207. }
  208. return Payment::query()
  209. ->where('gateway_provider', 'pagarme')
  210. ->where(function ($query) use ($metadataId, $chargeId, $transactionId, $orderId) {
  211. $query
  212. ->when($metadataId, fn ($query) => $query->orWhere('id', $metadataId))
  213. ->when($chargeId, fn ($query) => $query->orWhere('gateway_entity_reference', $chargeId))
  214. ->when($transactionId, fn ($query) => $query->orWhere('gateway_operation_reference', $transactionId))
  215. ->when($orderId, fn ($query) => $query->orWhere(function ($query) use ($orderId) {
  216. $query->where('gateway_entity_label', 'order')
  217. ->where('gateway_entity_reference', $orderId);
  218. }));
  219. })
  220. ->latest('id')
  221. ->first();
  222. }
  223. private function normalizeOrderResponse(?string $event, array $data): array
  224. {
  225. if (str_starts_with((string) $event, 'order.')) {
  226. return $data;
  227. }
  228. return [
  229. 'id' => data_get($data, 'order.id', data_get($data, 'order_id')),
  230. 'metadata' => data_get($data, 'metadata', data_get($data, 'order.metadata', [])),
  231. 'charges' => [$data],
  232. ];
  233. }
  234. }