PagarmePaymentService.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. <?php
  2. namespace App\Services\Pagarme;
  3. use App\Data\Pagarme\Request\PagarmeOrderRequestData\PagarmeOrderRequestData;
  4. use App\Data\Pagarme\Response\PagarmeOrderResponseData\PagarmeOrderResponseData;
  5. use App\Models\Payment;
  6. use App\Models\PaymentTransfer;
  7. use App\Services\Pagarme\Concerns\SendsPagarmeRequests;
  8. use Illuminate\Support\Collection;
  9. use Illuminate\Support\Str;
  10. class PagarmePaymentService
  11. {
  12. use SendsPagarmeRequests;
  13. public function createOrderWithCreditCard(
  14. Payment $payment,
  15. array $items,
  16. array $customer,
  17. array $creditCard,
  18. array $options = []
  19. ): array {
  20. $paymentMethod = [
  21. 'payment_method' => 'credit_card',
  22. 'credit_card' => $this->buildCreditCardPayload($creditCard),
  23. ];
  24. if (! empty($options['split']) && is_array($options['split'])) {
  25. $paymentMethod['split'] = $options['split'];
  26. }
  27. return $this->createOrder(
  28. payment: $payment,
  29. items: $items,
  30. customer: $customer,
  31. paymentMethod: $paymentMethod,
  32. options: $options,
  33. );
  34. }
  35. public function createOrderWithPix(
  36. Payment $payment,
  37. array $items,
  38. array $customer,
  39. array $pix,
  40. array $options = []
  41. ): array {
  42. $paymentMethod = [
  43. 'payment_method' => 'pix',
  44. 'pix' => $this->buildPixPayload($pix),
  45. ];
  46. if (! empty($options['split']) && is_array($options['split'])) {
  47. $paymentMethod['split'] = $options['split'];
  48. }
  49. return $this->createOrder(
  50. payment: $payment,
  51. items: $items,
  52. customer: $customer,
  53. paymentMethod: $paymentMethod,
  54. options: $options,
  55. );
  56. }
  57. //
  58. public function createOrder(
  59. Payment $payment,
  60. array $items,
  61. array $customer,
  62. array $paymentMethod,
  63. array $options = []
  64. ): array {
  65. if (empty($items)) {
  66. throw new \InvalidArgumentException('items nao pode estar vazio.');
  67. }
  68. if (empty($paymentMethod['payment_method'])) {
  69. throw new \InvalidArgumentException('payment_method e obrigatorio.');
  70. }
  71. if (! in_array($paymentMethod['payment_method'], ['credit_card', 'pix'], true)) {
  72. throw new \InvalidArgumentException('payment_method deve ser credit_card ou pix.');
  73. }
  74. $customerIdPayload = $options['customer_id'] ?? null;
  75. $customerObjectPayload = $this->filterFilledRecursive($customer);
  76. if (! $this->filled($customerIdPayload) && empty($customerObjectPayload)) {
  77. throw new \InvalidArgumentException('customer ou customer_id e obrigatorio.');
  78. }
  79. $requestData = new PagarmeOrderRequestData(
  80. code: $this->ensurePaymentCode($payment),
  81. items: $this->validateItems($items),
  82. payments: [$this->filterFilledRecursive($paymentMethod)],
  83. metadata: array_merge([
  84. 'payment_id' => (string) $payment->id,
  85. 'schedule_id' => (string) $payment->schedule_id,
  86. 'client_id' => (string) $payment->client_id,
  87. 'provider_id' => (string) $payment->provider_id,
  88. ], $options['metadata'] ?? []),
  89. customer: ! empty($customerObjectPayload) ? $customerObjectPayload : null,
  90. customerId: $this->filled($customerIdPayload) ? (string) $customerIdPayload : null,
  91. closed: $options['closed'] ?? true,
  92. channel: $options['channel'] ?? null,
  93. );
  94. $order = PagarmeOrderResponseData::fromArray($this->pagarmeRequest(
  95. method: 'POST',
  96. path: '/orders',
  97. payload: $requestData,
  98. idempotencyKey: $this->idempotencyKey($payment),
  99. errorMessage: 'Erro ao criar pedido de pagamento no Pagar.me.',
  100. ));
  101. if (empty($order->id())) {
  102. throw new \RuntimeException('Pagar.me order creation returned an empty id.');
  103. }
  104. return $order->toArray();
  105. }
  106. private function ensurePaymentCode(Payment $payment): string
  107. {
  108. if ($this->hasUuidCode($payment->gateway_code, 'payment')) {
  109. return $payment->gateway_code;
  110. }
  111. $code = 'payment-'.(string) Str::uuid();
  112. $payment->forceFill(['gateway_code' => $code])->save();
  113. return $code;
  114. }
  115. private function hasUuidCode(?string $code, string $prefix): bool
  116. {
  117. return is_string($code)
  118. && preg_match("/^{$prefix}-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i", $code) === 1;
  119. }
  120. //
  121. public function applyGatewayResponseToPayment(Payment $payment, array $orderResponse): Payment
  122. {
  123. $charge = $orderResponse['charges'][0] ?? [];
  124. $transaction = $charge['last_transaction'] ?? [];
  125. $chargeStatus = $charge['status'] ?? null;
  126. $transactionStatus = $transaction['status'] ?? null;
  127. $payment->forceFill([
  128. 'gateway_provider' => 'pagarme',
  129. 'gateway_entity_reference' => $charge['id'] ?? $orderResponse['id'] ?? null,
  130. 'gateway_entity_label' => isset($charge['id']) ? 'charge' : 'order',
  131. 'gateway_operation_reference' => $transaction['id'] ?? $charge['id'] ?? $orderResponse['id'] ?? null,
  132. 'gateway_operation_label' => isset($transaction['id']) ? 'transaction' : (isset($charge['id']) ? 'charge' : 'order'),
  133. 'status' => $this->mapPaymentStatus($chargeStatus, $transactionStatus),
  134. 'paid_at' => $this->filledArrayValue($charge, 'paid_at'),
  135. 'authorized_at' => $this->resolveAuthorizedAt($transactionStatus, $transaction),
  136. 'gateway_payload' => $orderResponse,
  137. 'failure_code' => $this->extractFailureCode($transaction),
  138. 'failure_message' => $this->extractFailureMessage($transaction),
  139. ])->save();
  140. return $payment->fresh();
  141. }
  142. public function buildSplitFromTransfers(Collection $transfers): array
  143. {
  144. return $transfers
  145. ->filter(fn (PaymentTransfer $transfer) => ! empty($transfer->gateway_transfer_target_reference))
  146. ->map(function (PaymentTransfer $transfer) {
  147. return [
  148. 'amount' => $this->toGatewayAmountInCents((float) $transfer->gross_amount),
  149. 'recipient_id' => $transfer->gateway_transfer_target_reference,
  150. 'type' => 'flat',
  151. 'options' => [
  152. 'charge_processing_fee' => false,
  153. 'charge_remainder_fee' => false,
  154. 'liable' => false,
  155. ],
  156. ];
  157. })
  158. ->values()
  159. ->all();
  160. }
  161. public function toGatewayAmountInCents(float $amount): int
  162. {
  163. return (int) round($amount * 100);
  164. }
  165. //
  166. private function idempotencyKey(Payment $payment): string
  167. {
  168. return "payment-{$payment->id}-schedule-{$payment->schedule_id}";
  169. }
  170. private function buildCreditCardPayload(array $creditCard): array
  171. {
  172. $payload = [];
  173. foreach ([
  174. 'installments',
  175. 'statement_descriptor',
  176. 'operation_type',
  177. 'recurrence_cycle',
  178. 'metadata',
  179. 'extended_limit_enabled',
  180. 'extended_limit_code',
  181. 'merchant_category_code',
  182. 'authentication',
  183. 'auto_recovery',
  184. 'payload',
  185. 'payment_type',
  186. 'funding_source',
  187. 'initiated_type',
  188. 'recurrence_model',
  189. 'channel',
  190. 'payment_origin',
  191. ] as $field) {
  192. if (array_key_exists($field, $creditCard) && $this->filled($creditCard[$field])) {
  193. $payload[$field] = $creditCard[$field];
  194. }
  195. }
  196. $allowedCardOptions = ['card', 'card_id', 'card_token', 'network_token'];
  197. $provided = array_values(array_filter(
  198. $allowedCardOptions,
  199. static fn (string $field) => ! empty($creditCard[$field])
  200. ));
  201. if (count($provided) !== 1) {
  202. throw new \InvalidArgumentException('Informe exatamente uma opcao entre card, card_id, card_token ou network_token.');
  203. }
  204. $selected = $provided[0];
  205. $payload[$selected] = $creditCard[$selected];
  206. return $payload;
  207. }
  208. private function buildPixPayload(array $pix): array
  209. {
  210. if (! $this->filled($pix['expires_in'] ?? null) && ! $this->filled($pix['expires_at'] ?? null)) {
  211. throw new \InvalidArgumentException('pix.expires_in ou pix.expires_at e obrigatorio.');
  212. }
  213. $payload = [];
  214. foreach (['expires_in', 'expires_at', 'additional_information'] as $field) {
  215. if (array_key_exists($field, $pix) && $this->filled($pix[$field])) {
  216. $payload[$field] = $pix[$field];
  217. }
  218. }
  219. return $payload;
  220. }
  221. private function extractFailureCode(array $transaction): ?string
  222. {
  223. return $this->filledArrayValue($transaction['gateway_response'] ?? [], 'code');
  224. }
  225. private function extractFailureMessage(array $transaction): ?string
  226. {
  227. $acquirerMessage = $this->filledArrayValue($transaction, 'acquirer_message');
  228. if ($acquirerMessage) {
  229. return $acquirerMessage;
  230. }
  231. $gatewayErrors = $transaction['gateway_response']['errors'] ?? [];
  232. if (! is_array($gatewayErrors) || empty($gatewayErrors)) {
  233. return null;
  234. }
  235. $message = collect($gatewayErrors)
  236. ->pluck('message')
  237. ->filter()
  238. ->implode('; ') ?: null;
  239. return $this->translateGatewayMessage($message);
  240. }
  241. private function filled(mixed $value): bool
  242. {
  243. return $value !== null && $value !== '' && $value !== [];
  244. }
  245. private function filledArrayValue(array $data, string $field): ?string
  246. {
  247. if (! array_key_exists($field, $data) || ! $this->filled($data[$field])) {
  248. return null;
  249. }
  250. return (string) $data[$field];
  251. }
  252. private function filterFilledRecursive(array $data): array
  253. {
  254. $filtered = [];
  255. foreach ($data as $key => $value) {
  256. if (is_array($value)) {
  257. $value = $this->filterFilledRecursive($value);
  258. }
  259. if ($this->filled($value)) {
  260. $filtered[$key] = $value;
  261. }
  262. }
  263. return $filtered;
  264. }
  265. private function translateGatewayMessage(?string $message): ?string
  266. {
  267. if (! $message) {
  268. return null;
  269. }
  270. if (str_contains($message, 'Sem ambiente configurado')) {
  271. return 'Pix não esta habilitado ou configurado neste ambiente do Pagar.me.';
  272. }
  273. return $message;
  274. }
  275. private function mapPaymentStatus(?string $chargeStatus, ?string $transactionStatus): string
  276. {
  277. $status = strtolower((string) ($transactionStatus ?: $chargeStatus));
  278. return match ($status) {
  279. 'captured', 'paid', 'overpaid' => 'paid',
  280. 'authorized_pending_capture', 'waiting_capture' => 'authorized',
  281. 'pending', 'waiting_payment' => 'pending',
  282. 'processing' => 'processing',
  283. 'not_authorized', 'with_error', 'failed',
  284. 'underpaid', 'chargedback' => 'failed',
  285. 'voided', 'partial_void', 'canceled',
  286. 'cancelled', 'refunded', 'partial_refunded',
  287. 'partial_canceled' => 'cancelled',
  288. default => 'pending',
  289. };
  290. }
  291. private function resolveAuthorizedAt(?string $transactionStatus, array $transaction): ?string
  292. {
  293. if (in_array($transactionStatus, ['authorized_pending_capture', 'captured', 'partial_capture'], true)) {
  294. return $this->filledArrayValue($transaction, 'created_at');
  295. }
  296. return null;
  297. }
  298. private function validateItems(array $items): array
  299. {
  300. return collect($items)
  301. ->map(function (array $item, int $index) {
  302. foreach (['code', 'amount', 'quantity'] as $field) {
  303. if (! array_key_exists($field, $item) || ! $this->filled($item[$field])) {
  304. throw new \InvalidArgumentException("items.{$index}.{$field} e obrigatorio.");
  305. }
  306. }
  307. if ((int) $item['amount'] <= 0 || (int) $item['quantity'] <= 0) {
  308. throw new \InvalidArgumentException("items.{$index}.amount e quantity devem ser maiores que zero.");
  309. }
  310. return $this->filterFilledRecursive($item);
  311. })
  312. ->values()
  313. ->all();
  314. }
  315. }