|
|
@@ -4,11 +4,11 @@ namespace App\Services\Pagarme;
|
|
|
|
|
|
use App\Data\Pagarme\Request\PagarmeOrderRequestData\PagarmeOrderRequestData;
|
|
|
use App\Data\Pagarme\Response\PagarmeOrderResponseData\PagarmeOrderResponseData;
|
|
|
+use App\Enums\PaymentSplitStatusEnum;
|
|
|
use App\Enums\PaymentStatusEnum;
|
|
|
use App\Models\Payment;
|
|
|
use App\Models\PaymentSplit;
|
|
|
use App\Services\Pagarme\Concerns\SendsPagarmeRequests;
|
|
|
-use Illuminate\Support\Collection;
|
|
|
use Illuminate\Support\Str;
|
|
|
|
|
|
class PagarmePaymentService
|
|
|
@@ -22,20 +22,14 @@ class PagarmePaymentService
|
|
|
array $creditCard,
|
|
|
array $options = []
|
|
|
): array {
|
|
|
- $paymentMethod = [
|
|
|
- 'payment_method' => 'credit_card',
|
|
|
- 'credit_card' => $this->buildCreditCardPayload($creditCard),
|
|
|
- ];
|
|
|
-
|
|
|
- if (! empty($options['split']) && is_array($options['split'])) {
|
|
|
- $paymentMethod['split'] = $options['split'];
|
|
|
- }
|
|
|
-
|
|
|
return $this->createOrder(
|
|
|
payment: $payment,
|
|
|
items: $items,
|
|
|
customer: $customer,
|
|
|
- paymentMethod: $paymentMethod,
|
|
|
+ paymentMethod: PagarmeOrderRequestData::creditCardPaymentMethod(
|
|
|
+ creditCard: $creditCard,
|
|
|
+ split: is_array($options['split'] ?? null) ? $options['split'] : null,
|
|
|
+ ),
|
|
|
options: $options,
|
|
|
);
|
|
|
}
|
|
|
@@ -47,25 +41,19 @@ class PagarmePaymentService
|
|
|
array $pix,
|
|
|
array $options = []
|
|
|
): array {
|
|
|
- $paymentMethod = [
|
|
|
- 'payment_method' => 'pix',
|
|
|
- 'pix' => $this->buildPixPayload($pix),
|
|
|
- ];
|
|
|
-
|
|
|
- if (! empty($options['split']) && is_array($options['split'])) {
|
|
|
- $paymentMethod['split'] = $options['split'];
|
|
|
- }
|
|
|
-
|
|
|
return $this->createOrder(
|
|
|
payment: $payment,
|
|
|
items: $items,
|
|
|
customer: $customer,
|
|
|
- paymentMethod: $paymentMethod,
|
|
|
+ paymentMethod: PagarmeOrderRequestData::pixPaymentMethod(
|
|
|
+ pix: $pix,
|
|
|
+ split: is_array($options['split'] ?? null) ? $options['split'] : null,
|
|
|
+ ),
|
|
|
options: $options,
|
|
|
);
|
|
|
}
|
|
|
|
|
|
- //
|
|
|
+ // criacao de pedidos por metodo de pagamento
|
|
|
|
|
|
public function createOrder(
|
|
|
Payment $payment,
|
|
|
@@ -74,37 +62,18 @@ class PagarmePaymentService
|
|
|
array $paymentMethod,
|
|
|
array $options = []
|
|
|
): array {
|
|
|
- if (empty($items)) {
|
|
|
- throw new \InvalidArgumentException('items nao pode estar vazio.');
|
|
|
- }
|
|
|
-
|
|
|
- if (empty($paymentMethod['payment_method'])) {
|
|
|
- throw new \InvalidArgumentException('payment_method e obrigatorio.');
|
|
|
- }
|
|
|
-
|
|
|
- if (! in_array($paymentMethod['payment_method'], ['credit_card', 'pix'], true)) {
|
|
|
- throw new \InvalidArgumentException('payment_method deve ser credit_card ou pix.');
|
|
|
- }
|
|
|
-
|
|
|
- $customerIdPayload = $options['customer_id'] ?? null;
|
|
|
- $customerObjectPayload = $this->filterFilledRecursive($customer);
|
|
|
-
|
|
|
- if (! $this->filled($customerIdPayload) && empty($customerObjectPayload)) {
|
|
|
- throw new \InvalidArgumentException('customer ou customer_id e obrigatorio.');
|
|
|
- }
|
|
|
-
|
|
|
- $requestData = new PagarmeOrderRequestData(
|
|
|
+ $requestData = PagarmeOrderRequestData::fromOrderPayload(
|
|
|
code: $this->ensurePaymentCode($payment),
|
|
|
- items: $this->validateItems($items),
|
|
|
- payments: [$this->filterFilledRecursive($paymentMethod)],
|
|
|
+ items: $items,
|
|
|
+ customer: $customer,
|
|
|
+ paymentMethod: $paymentMethod,
|
|
|
metadata: array_merge([
|
|
|
'payment_id' => (string) $payment->id,
|
|
|
'schedule_id' => (string) $payment->schedule_id,
|
|
|
'client_id' => (string) $payment->client_id,
|
|
|
'provider_id' => (string) $payment->provider_id,
|
|
|
], $options['metadata'] ?? []),
|
|
|
- customer: ! empty($customerObjectPayload) ? $customerObjectPayload : null,
|
|
|
- customerId: $this->filled($customerIdPayload) ? (string) $customerIdPayload : null,
|
|
|
+ customerId: $options['customer_id'] ?? null,
|
|
|
closed: $options['closed'] ?? true,
|
|
|
channel: $options['channel'] ?? null,
|
|
|
);
|
|
|
@@ -117,16 +86,21 @@ class PagarmePaymentService
|
|
|
errorMessage: 'Erro ao criar pedido de pagamento no Pagar.me.',
|
|
|
));
|
|
|
|
|
|
- if (empty($order->id())) {
|
|
|
- throw new \RuntimeException('Pagar.me order creation returned an empty id.');
|
|
|
- }
|
|
|
+ $order->requireId();
|
|
|
|
|
|
return $order->toArray();
|
|
|
}
|
|
|
|
|
|
+ // evita criacao duplicada de pedidos para o mesmo pagamento
|
|
|
+
|
|
|
+ private function idempotencyKey(Payment $payment): string
|
|
|
+ {
|
|
|
+ return "payment-{$payment->id}-schedule-{$payment->schedule_id}";
|
|
|
+ }
|
|
|
+
|
|
|
private function ensurePaymentCode(Payment $payment): string
|
|
|
{
|
|
|
- if ($this->hasUuidCode($payment->gateway_code, 'payment')) {
|
|
|
+ if (! empty($payment->gateway_code)) {
|
|
|
return $payment->gateway_code;
|
|
|
}
|
|
|
|
|
|
@@ -137,252 +111,46 @@ class PagarmePaymentService
|
|
|
return $code;
|
|
|
}
|
|
|
|
|
|
- private function hasUuidCode(?string $code, string $prefix): bool
|
|
|
- {
|
|
|
- return is_string($code)
|
|
|
- && 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;
|
|
|
- }
|
|
|
-
|
|
|
//
|
|
|
|
|
|
public function applyGatewayResponseToPayment(Payment $payment, array $orderResponse): Payment
|
|
|
{
|
|
|
- $charge = $orderResponse['charges'][0] ?? [];
|
|
|
-
|
|
|
- $transaction = $charge['last_transaction'] ?? [];
|
|
|
+ $order = PagarmeOrderResponseData::fromArray($orderResponse);
|
|
|
+ $newStatus = $order->paymentStatus();
|
|
|
+ $failureCode = null;
|
|
|
+ $failureMessage = null;
|
|
|
|
|
|
- $chargeStatus = $charge['status'] ?? null;
|
|
|
-
|
|
|
- $transactionStatus = $transaction['status'] ?? null;
|
|
|
+ if ($newStatus === PaymentStatusEnum::FAILED) {
|
|
|
+ $failureCode = $order->failureCode();
|
|
|
+ $failureMessage = $order->failureMessage();
|
|
|
+ }
|
|
|
|
|
|
$payment->forceFill([
|
|
|
'gateway_provider' => 'pagarme',
|
|
|
- 'gateway_entity_reference' => $charge['id'] ?? $orderResponse['id'] ?? null,
|
|
|
- 'gateway_entity_label' => isset($charge['id']) ? 'charge' : 'order',
|
|
|
- 'gateway_operation_reference' => $transaction['id'] ?? $charge['id'] ?? $orderResponse['id'] ?? null,
|
|
|
- 'gateway_operation_label' => isset($transaction['id']) ? 'transaction' : (isset($charge['id']) ? 'charge' : 'order'),
|
|
|
- 'status' => $this->mapPaymentStatus($chargeStatus, $transactionStatus),
|
|
|
- 'paid_at' => $this->filledArrayValue($charge, 'paid_at'),
|
|
|
- 'authorized_at' => $this->resolveAuthorizedAt($transactionStatus, $transaction),
|
|
|
+ 'gateway_entity_reference' => $order->gatewayEntityReference(),
|
|
|
+ 'gateway_entity_label' => $order->gatewayEntityLabel(),
|
|
|
+ 'gateway_operation_reference' => $order->gatewayOperationReference(),
|
|
|
+ 'gateway_operation_label' => $order->gatewayOperationLabel(),
|
|
|
+ 'status' => $newStatus,
|
|
|
+ 'paid_at' => $order->paidAt(),
|
|
|
+ 'authorized_at' => $order->authorizedAt(),
|
|
|
'gateway_payload' => $orderResponse,
|
|
|
- 'failure_code' => $this->extractFailureCode($transaction),
|
|
|
- 'failure_message' => $this->extractFailureMessage($transaction),
|
|
|
+ 'failure_code' => $failureCode,
|
|
|
+ 'failure_message' => $failureMessage,
|
|
|
])->save();
|
|
|
|
|
|
- return $payment->fresh();
|
|
|
- }
|
|
|
-
|
|
|
- public function buildSplitFromTransfers(Collection $transfers): array
|
|
|
- {
|
|
|
- return $transfers
|
|
|
- ->filter(fn (PaymentSplit $split) => ! empty($split->gateway_transfer_target_reference))
|
|
|
- ->map(function (PaymentSplit $split) {
|
|
|
- return [
|
|
|
- 'amount' => $this->toGatewayAmountInCents((float) $split->gross_amount),
|
|
|
- 'recipient_id' => $split->gateway_transfer_target_reference,
|
|
|
- 'type' => 'flat',
|
|
|
-
|
|
|
- 'options' => [
|
|
|
- 'charge_processing_fee' => false,
|
|
|
- 'charge_remainder_fee' => false,
|
|
|
- 'liable' => false,
|
|
|
- ],
|
|
|
- ];
|
|
|
- })
|
|
|
- ->values()
|
|
|
- ->all();
|
|
|
- }
|
|
|
-
|
|
|
- public function toGatewayAmountInCents(float $amount): int
|
|
|
- {
|
|
|
- return (int) round($amount * 100);
|
|
|
- }
|
|
|
-
|
|
|
- //
|
|
|
-
|
|
|
- private function idempotencyKey(Payment $payment): string
|
|
|
- {
|
|
|
- return "payment-{$payment->id}-schedule-{$payment->schedule_id}";
|
|
|
- }
|
|
|
-
|
|
|
- private function buildCreditCardPayload(array $creditCard): array
|
|
|
- {
|
|
|
- $payload = [];
|
|
|
-
|
|
|
- foreach ([
|
|
|
- 'installments',
|
|
|
- 'statement_descriptor',
|
|
|
- 'operation_type',
|
|
|
- 'recurrence_cycle',
|
|
|
- 'metadata',
|
|
|
- 'extended_limit_enabled',
|
|
|
- 'extended_limit_code',
|
|
|
- 'merchant_category_code',
|
|
|
- 'authentication',
|
|
|
- 'auto_recovery',
|
|
|
- 'payload',
|
|
|
- 'payment_type',
|
|
|
- 'funding_source',
|
|
|
- 'initiated_type',
|
|
|
- 'recurrence_model',
|
|
|
- 'channel',
|
|
|
- 'payment_origin',
|
|
|
- ] as $field) {
|
|
|
- if (array_key_exists($field, $creditCard) && $this->filled($creditCard[$field])) {
|
|
|
- $payload[$field] = $creditCard[$field];
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- $allowedCardOptions = ['card', 'card_id', 'card_token', 'network_token'];
|
|
|
-
|
|
|
- $provided = array_values(array_filter(
|
|
|
- $allowedCardOptions,
|
|
|
- static fn (string $field) => ! empty($creditCard[$field])
|
|
|
- ));
|
|
|
-
|
|
|
- if (count($provided) !== 1) {
|
|
|
- throw new \InvalidArgumentException('Informe exatamente uma opcao entre card, card_id, card_token ou network_token.');
|
|
|
- }
|
|
|
-
|
|
|
- $selected = $provided[0];
|
|
|
-
|
|
|
- $payload[$selected] = $creditCard[$selected];
|
|
|
-
|
|
|
- return $payload;
|
|
|
- }
|
|
|
-
|
|
|
- private function buildPixPayload(array $pix): array
|
|
|
- {
|
|
|
- if (! $this->filled($pix['expires_in'] ?? null) && ! $this->filled($pix['expires_at'] ?? null)) {
|
|
|
- throw new \InvalidArgumentException('pix.expires_in ou pix.expires_at e obrigatorio.');
|
|
|
- }
|
|
|
-
|
|
|
- $payload = [];
|
|
|
-
|
|
|
- foreach (['expires_in', 'expires_at', 'additional_information'] as $field) {
|
|
|
- if (array_key_exists($field, $pix) && $this->filled($pix[$field])) {
|
|
|
- $payload[$field] = $pix[$field];
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return $payload;
|
|
|
- }
|
|
|
-
|
|
|
- private function extractFailureCode(array $transaction): ?string
|
|
|
- {
|
|
|
- return $this->filledArrayValue($transaction['gateway_response'] ?? [], 'code');
|
|
|
- }
|
|
|
-
|
|
|
- private function extractFailureMessage(array $transaction): ?string
|
|
|
- {
|
|
|
- $acquirerMessage = $this->filledArrayValue($transaction, 'acquirer_message');
|
|
|
-
|
|
|
- if ($acquirerMessage) {
|
|
|
- return $acquirerMessage;
|
|
|
- }
|
|
|
-
|
|
|
- $gatewayErrors = $transaction['gateway_response']['errors'] ?? [];
|
|
|
-
|
|
|
- if (! is_array($gatewayErrors) || empty($gatewayErrors)) {
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- $message = collect($gatewayErrors)
|
|
|
- ->pluck('message')
|
|
|
- ->filter()
|
|
|
- ->implode('; ') ?: null;
|
|
|
-
|
|
|
- return $this->translateGatewayMessage($message);
|
|
|
- }
|
|
|
-
|
|
|
- private function filled(mixed $value): bool
|
|
|
- {
|
|
|
- return $value !== null && $value !== '' && $value !== [];
|
|
|
- }
|
|
|
-
|
|
|
- private function filledArrayValue(array $data, string $field): ?string
|
|
|
- {
|
|
|
- if (! array_key_exists($field, $data) || ! $this->filled($data[$field])) {
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- return (string) $data[$field];
|
|
|
- }
|
|
|
-
|
|
|
- private function filterFilledRecursive(array $data): array
|
|
|
- {
|
|
|
- $filtered = [];
|
|
|
-
|
|
|
- foreach ($data as $key => $value) {
|
|
|
- if (is_array($value)) {
|
|
|
- $value = $this->filterFilledRecursive($value);
|
|
|
- }
|
|
|
-
|
|
|
- if ($this->filled($value)) {
|
|
|
- $filtered[$key] = $value;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return $filtered;
|
|
|
- }
|
|
|
-
|
|
|
- private function translateGatewayMessage(?string $message): ?string
|
|
|
- {
|
|
|
- if (! $message) {
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- if (str_contains($message, 'Sem ambiente configurado')) {
|
|
|
- return 'Pix não esta habilitado ou configurado neste ambiente do Pagar.me.';
|
|
|
- }
|
|
|
-
|
|
|
- return $message;
|
|
|
- }
|
|
|
-
|
|
|
- private function mapPaymentStatus(?string $chargeStatus, ?string $transactionStatus): PaymentStatusEnum
|
|
|
- {
|
|
|
- $status = strtolower((string) ($transactionStatus ?: $chargeStatus));
|
|
|
-
|
|
|
- return match ($status) {
|
|
|
- 'captured', 'paid', 'overpaid' => PaymentStatusEnum::PAID,
|
|
|
- 'authorized_pending_capture', 'waiting_capture' => PaymentStatusEnum::AUTHORIZED,
|
|
|
- 'pending', 'waiting_payment' => PaymentStatusEnum::PENDING,
|
|
|
- 'processing' => PaymentStatusEnum::PROCESSING,
|
|
|
- 'not_authorized', 'with_error', 'failed',
|
|
|
- 'underpaid', 'chargedback' => PaymentStatusEnum::FAILED,
|
|
|
- 'voided', 'partial_void', 'canceled',
|
|
|
- 'cancelled', 'refunded', 'partial_refunded',
|
|
|
- 'partial_canceled' => PaymentStatusEnum::CANCELLED,
|
|
|
- default => PaymentStatusEnum::PENDING,
|
|
|
+ $splitStatus = match ($newStatus) {
|
|
|
+ PaymentStatusEnum::PAID => PaymentSplitStatusEnum::TRANSFERRED,
|
|
|
+ PaymentStatusEnum::FAILED => PaymentSplitStatusEnum::FAILED,
|
|
|
+ PaymentStatusEnum::CANCELLED => PaymentSplitStatusEnum::CANCELLED,
|
|
|
+ PaymentStatusEnum::AUTHORIZED => PaymentSplitStatusEnum::PROCESSING,
|
|
|
+ default => PaymentSplitStatusEnum::PENDING,
|
|
|
};
|
|
|
- }
|
|
|
-
|
|
|
- private function resolveAuthorizedAt(?string $transactionStatus, array $transaction): ?string
|
|
|
- {
|
|
|
- if (in_array($transactionStatus, ['authorized_pending_capture', 'captured', 'partial_capture'], true)) {
|
|
|
- return $this->filledArrayValue($transaction, 'created_at');
|
|
|
- }
|
|
|
|
|
|
- return null;
|
|
|
- }
|
|
|
+ PaymentSplit::query()
|
|
|
+ ->where('payment_id', $payment->id)
|
|
|
+ ->update(['status' => $splitStatus]);
|
|
|
|
|
|
- private function validateItems(array $items): array
|
|
|
- {
|
|
|
- return collect($items)
|
|
|
- ->map(function (array $item, int $index) {
|
|
|
- foreach (['code', 'amount', 'quantity'] as $field) {
|
|
|
- if (! array_key_exists($field, $item) || ! $this->filled($item[$field])) {
|
|
|
- throw new \InvalidArgumentException("items.{$index}.{$field} e obrigatorio.");
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if ((int) $item['amount'] <= 0 || (int) $item['quantity'] <= 0) {
|
|
|
- throw new \InvalidArgumentException("items.{$index}.amount e quantity devem ser maiores que zero.");
|
|
|
- }
|
|
|
-
|
|
|
- return $this->filterFilledRecursive($item);
|
|
|
- })
|
|
|
- ->values()
|
|
|
- ->all();
|
|
|
+ return $payment->fresh();
|
|
|
}
|
|
|
}
|