Procházet zdrojové kódy

feature: add service para pagamento com pagarme

Gustavo Mantovani před 3 týdny
rodič
revize
f5fcb7c80f

+ 5 - 3
app/Services/Pagarme/PagarmeCustomerService.php

@@ -16,8 +16,9 @@ class PagarmeCustomerService
 
         $client->loadMissing('user');
 
-        $name     = $client->user?->name ?? $data['name'] ?? 'Cliente';
-        $email    = $client->user?->email ?? $data['email'] ?? null;
+        $name  = $client->user?->name ?? $data['name'] ?? 'Cliente';
+        $email = $client->user?->email ?? $data['email'] ?? null;
+
         $document = $this->sanitizeDigits($client->document ?? $data['document'] ?? null);
 
         if (empty($email) || empty($document)) {
@@ -34,7 +35,8 @@ class PagarmeCustomerService
 
         $address = $this->buildAddress($data);
         $phones  = $this->buildPhones($client->user?->phone ?? $data['phone'] ?? null);
-        $code    = $client->external_customer_code ?? "client-{$client->id}";
+
+        $code = $client->external_customer_code ?? "client-{$client->id}";
 
         $payload = [
             'name'          => $name,

+ 303 - 0
app/Services/Pagarme/PagarmePaymentService.php

@@ -0,0 +1,303 @@
+<?php
+
+namespace App\Services\Pagarme;
+
+use App\Models\Payment;
+use App\Models\PaymentTransfer;
+use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Http;
+use Illuminate\Support\Facades\Log;
+
+class PagarmePaymentService
+{
+    public function createOrderWithCreditCard(
+        Payment $payment, array $items, array $customer, 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,
+            options: $options,
+        );
+    }
+
+    public function createOrderWithPix(
+        Payment $payment,
+        array $items,
+        array $customer,
+        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,
+            options: $options,
+        );
+    }
+
+    public function createOrder(
+        Payment $payment,
+        array $items,
+        array $customer,
+        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.');
+        }
+
+        $payload = [
+            'code'     => $options['order_code'] ?? "payment-{$payment->id}",
+            'items'    => $items,
+            'customer' => $customer,
+            'payments' => [$paymentMethod],
+            'closed'   => $options['closed'] ?? true,
+
+            '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'] ?? []),
+        ];
+
+        if (! empty($options['channel'])) {
+            $payload['channel'] = $options['channel'];
+        }
+
+        $response = $this->pagarmeRequest($this->idempotencyKey($payment))
+            ->post($this->pagarmeUrl('/orders'), $payload);
+
+        if ($response->failed()) {
+            Log::channel('pagarme')->error('Pagar.me order creation failed', [
+                'status'  => $response->status(),
+                'body'    => $response->json() ?? $response->body(),
+                'payload' => $payload,
+            ]);
+
+            throw new \RuntimeException('Erro ao criar pedido de pagamento no Pagar.me.');
+        }
+
+        $order = $response->json();
+
+        if (empty($order['id'])) {
+            Log::channel('pagarme')->error('Pagar.me order creation returned empty id', [
+                'payment_id' => $payment->id,
+                'response'   => $order,
+            ]);
+
+            throw new \RuntimeException('Pagar.me order creation returned an empty id.');
+        }
+
+        return $order;
+    }
+
+    public function applyGatewayResponseToPayment(Payment $payment, array $orderResponse): Payment
+    {
+        $charge            = $orderResponse['charges'][0] ?? [];
+        $transaction       = $charge['last_transaction'] ?? [];
+        $chargeStatus      = $charge['status'] ?? null;
+        $transactionStatus = $transaction['status'] ?? null;
+
+        $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'                     => $charge['paid_at'] ?? null,
+            'authorized_at'               => $this->resolveAuthorizedAt($transactionStatus, $transaction),
+            'gateway_payload'             => $orderResponse,
+            'failure_code'                => $this->extractFailureCode($transaction),
+            'failure_message'             => $this->extractFailureMessage($transaction),
+        ])->save();
+
+        return $payment->fresh();
+    }
+
+    /**
+     * @param  Collection<int, PaymentTransfer>  $transfers
+     * @return array<int, array<string, mixed>>
+     */
+    public function buildSplitFromTransfers(Collection $transfers): array
+    {
+        return $transfers
+            ->filter(fn (PaymentTransfer $transfer) => ! empty($transfer->gateway_transfer_target_reference))
+            ->map(function (PaymentTransfer $transfer) {
+                return [
+                    'amount'       => $this->toGatewayAmountInCents((float) $transfer->gross_amount),
+                    'recipient_id' => $transfer->gateway_transfer_target_reference,
+                    'type'         => 'flat',
+                ];
+            })
+            ->values()
+            ->all();
+    }
+
+    public function toGatewayAmountInCents(float $amount): int
+    {
+        return (int) round($amount * 100);
+    }
+
+    //
+
+    private function pagarmeUrl(string $path): string
+    {
+        return rtrim(config('services.pagarme.base_url'), '/').'/'.ltrim($path, '/');
+    }
+
+    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)) {
+                $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 (empty($pix['expires_in']) && empty($pix['expires_at'])) {
+            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)) {
+                $payload[$field] = $pix[$field];
+            }
+        }
+
+        return $payload;
+    }
+
+    //
+
+    private function resolveAuthorizedAt(?string $transactionStatus, array $transaction): ?string
+    {
+        if (in_array($transactionStatus, ['authorized_pending_capture', 'captured', 'partial_capture'], true)) {
+            return $transaction['created_at'] ?? now()->toISOString();
+        }
+
+        return null;
+    }
+
+    //
+
+    private function extractFailureCode(array $transaction): ?string
+    {
+        return $transaction['gateway_response']['code'] ?? null;
+    }
+
+    private function extractFailureMessage(array $transaction): ?string
+    {
+        return $transaction['acquirer_message'] ?? null;
+    }
+
+    //
+
+    private function mapPaymentStatus(?string $chargeStatus, ?string $transactionStatus): string
+    {
+        $status = strtolower((string) ($transactionStatus ?: $chargeStatus));
+
+        return match ($status) {
+            'captured', 'paid' => 'paid',
+            'authorized_pending_capture', 'waiting_capture', 'pending' => 'authorized',
+            'processing' => 'processing',
+            'not_authorized', 'with_error', 'failed' => 'failed',
+            'voided', 'partial_void' => 'cancelled',
+            default => 'pending',
+        };
+    }
+
+    //
+
+    private function pagarmeRequest(string $idempotencyKey)
+    {
+        $secretKey = config('services.pagarme.secret_key');
+
+        if (empty($secretKey)) {
+            Log::channel('pagarme')->error('PAGARME_SECRET_KEY is not configured.');
+
+            throw new \RuntimeException('PAGARME_SECRET_KEY is not configured.');
+        }
+
+        return Http::withBasicAuth($secretKey, '')
+            ->withHeaders([
+                'Idempotency-Key' => $idempotencyKey,
+                'Content-Type'    => 'application/json',
+                'Accept'          => 'application/json',
+            ]);
+    }
+}

+ 132 - 43
app/Services/Pagarme/PagarmeRecipientService.php

@@ -10,50 +10,75 @@ class PagarmeRecipientService
 {
     public function createRecipientForProvider(Provider $provider, array $data): string
     {
-        if (!empty($provider->recipient_id)) {
+        if (! empty($provider->recipient_id)) {
             return $provider->recipient_id;
         }
 
         $bankAccountData = $data['recipient_default_bank_account'];
         $metadata        = $data['recipient_metadata'] ?? [];
         $paymentMode     = $data['recipient_payment_mode'];
-
-        $response = $this->pagarmeRequest($provider->id)
-            ->post($this->pagarmeUrl('/recipients'), [
-                'default_bank_account' => [
-                    'holder_name'         => $bankAccountData['holder_name'],
-                    'holder_type'         => $bankAccountData['holder_type'],
-                    'holder_document'     => $bankAccountData['holder_document'],
-                    'bank'                => $bankAccountData['bank'],
-                    'branch_number'       => $bankAccountData['branch_number'],
-                    'branch_check_digit'  => $bankAccountData['branch_check_digit'] ?? null,
-                    'account_number'      => $bankAccountData['account_number'],
-                    'account_check_digit' => $bankAccountData['account_check_digit'],
-                    'type'                => $bankAccountData['type'],
-                    'metadata'            => $bankAccountData['metadata'] ?? [],
-                    'pix_key'             => $bankAccountData['pix_key'] ?? null,
+        $recipientType   = $data['recipient_type'] ?? 'individual';
+
+        $addressParts  = $this->extractAddressParts($data);
+        $monthlyIncome = isset($data['monthly_income']) ? (int) $data['monthly_income'] : 1000;
+        $occupation    = $data['professional_occupation'] ?? 'autonomo';
+
+        $payload = [
+            'code' => preg_replace('/\D+/', '', $data['recipient_code']),
+
+            'register_information' => [
+                'name'                    => $data['recipient_name'],
+                'email'                   => $data['recipient_email'],
+                'document'                => preg_replace('/\D+/', '', $data['recipient_document']),
+                'type'                    => $recipientType,
+                'birthdate'               => $data['birth_date'] ?? null,
+                'monthly_income'          => $monthlyIncome,
+                'professional_occupation' => $occupation,
+                'phone_numbers'           => $this->buildPhoneNumbers($data['phone'] ?? null),
+
+                'address' => [
+                    'street'          => $data['address'],
+                    'complementary'   => $addressParts['complementary'],
+                    'street_number'   => $addressParts['street_number'],
+                    'neighborhood'    => $addressParts['neighborhood'],
+                    'city'            => $data['city'] ?? null,
+                    'state'           => $data['state'] ?? null,
+                    'zip_code'        => preg_replace('/\D+/', '', $data['zip_code']),
+                    'reference_point' => $addressParts['reference_point'],
                 ],
+            ],
 
-                'metadata'     => $metadata,
-                'code'         => $data['recipient_code'],
-                'payment_mode' => $paymentMode,
-                'name'         => $data['recipient_name'],
-                'email'        => $data['recipient_email'],
-                'description'  => $data['recipient_description'],
-                'document'     => $data['recipient_document'],
-                'type'         => $data['recipient_type'],
-
-                'transfer_settings' => [
-                    'transfer_enabled'  => false,
-                    'transfer_interval' => 'daily',
-                    'transfer_day'      => 0,
-                ],
-            ]);
+            'default_bank_account' => [
+                'holder_name'         => $bankAccountData['holder_name'],
+                'holder_type'         => $bankAccountData['holder_type'],
+                'holder_document'     => preg_replace('/\D+/', '', $bankAccountData['holder_document']),
+                'bank'                => $bankAccountData['bank'],
+                'branch_number'       => $bankAccountData['branch_number'],
+                'branch_check_digit'  => $bankAccountData['branch_check_digit'] ?? null,
+                'account_number'      => $bankAccountData['account_number'],
+                'account_check_digit' => $bankAccountData['account_check_digit'],
+                'type'                => $bankAccountData['type'],
+            ],
+
+            'transfer_settings' => [
+                'transfer_enabled'  => false,
+                'transfer_interval' => 'Daily',
+                'transfer_day'      => 0,
+            ],
+
+            'automatic_anticipation_settings' => [
+                'enabled' => false,
+            ],
+        ];
+
+        $response = $this->pagarmeRequest($provider->id)
+            ->post($this->pagarmeUrl('/recipients'), $payload);
 
         if ($response->failed()) {
             Log::channel('pagarme')->error('Pagar.me recipient creation failed', [
-                'status' => $response->status(),
-                'body'   => $response->json() ?? $response->body(),
+                'status'  => $response->status(),
+                'body'    => $response->json() ?? $response->body(),
+                'payload' => $payload,
             ]);
 
             throw new \RuntimeException('Erro ao criar recebedor no Pagar.me.');
@@ -62,7 +87,7 @@ class PagarmeRecipientService
         $recipientData = $response->json();
         $recipientId   = $recipientData['id'] ?? null;
 
-        if (!$recipientId) {
+        if (! $recipientId) {
             Log::channel('pagarme')->error('Pagar.me recipient creation returned empty id', [
                 'response' => $recipientData,
             ]);
@@ -76,7 +101,7 @@ class PagarmeRecipientService
             'recipient_email'                => $data['recipient_email'],
             'recipient_description'          => $data['recipient_description'],
             'recipient_document'             => $data['recipient_document'],
-            'recipient_type'                 => $data['recipient_type'],
+            'recipient_type'                 => $recipientType,
             'recipient_code'                 => $data['recipient_code'],
             'recipient_payment_mode'         => $paymentMode,
             'recipient_default_bank_account' => $bankAccountData,
@@ -103,7 +128,7 @@ class PagarmeRecipientService
 
     private function pagarmeUrl(string $path): string
     {
-        return rtrim(config('services.pagarme.base_url'), '/') . '/' . ltrim($path, '/');
+        return rtrim(config('services.pagarme.base_url'), '/').'/'.ltrim($path, '/');
     }
 
     private function idempotencyKey(int $providerId, string $suffix = 'recipient'): string
@@ -133,19 +158,83 @@ class PagarmeRecipientService
 
     private function applyAutomaticAnticipationSettings(int $providerId, string $recipientId): void
     {
+        $payload = [
+            'enabled' => false,
+        ];
+
         $response = $this->pagarmeRequest($providerId, 'auto-anticipation')
-            ->patch($this->pagarmeUrl("/recipients/{$recipientId}/automatic-anticipation-settings"), [
-                'enabled' => false,
-            ]);
+            ->patch($this->pagarmeUrl("/recipients/{$recipientId}/automatic-anticipation-settings"), $payload);
 
         if ($response->failed()) {
             Log::channel('pagarme')->error('Pagar.me automatic anticipation settings update failed', [
-                'status'       => $response->status(),
-                'body'         => $response->json() ?? $response->body(),
-                'recipient_id' => $recipientId,
+                'status'  => $response->status(),
+                'body'    => $response->json() ?? $response->body(),
+                'payload' => $payload,
             ]);
 
             throw new \RuntimeException('Erro ao atualizar antecipação automática do recebedor no Pagar.me.');
         }
     }
-}
+
+    private function extractAddressParts(array $data): array
+    {
+        $addressLine = trim((string) ($data['address'] ?? ''));
+        $segments    = array_map('trim', explode(',', $addressLine));
+
+        $streetSegment  = $segments[0] ?? '';
+        $streetNumber   = $data['number'] ?? null;
+        $neighborhood   = $data['district'] ?? null;
+        $referencePoint = $data['reference_point'] ?? null;
+        $complementary  = $data['complement'] ?? null;
+
+        if ($streetNumber === null) {
+            preg_match('/^(\d+)/', $streetSegment, $matches);
+            $streetNumber = $matches[1] ?? 'S/N';
+        }
+
+        if ($neighborhood === null) {
+            $neighborhood = $segments[1] ?? 'N/A';
+        }
+
+        if ($referencePoint === null) {
+            $referencePoint = 'N/A';
+        }
+
+        if ($complementary === null) {
+            $complementary = 'N/A';
+        }
+
+        return [
+            'street_number'   => (string) $streetNumber,
+            'neighborhood'    => (string) $neighborhood,
+            'reference_point' => (string) $referencePoint,
+            'complementary'   => (string) $complementary,
+        ];
+    }
+
+    private function buildPhoneNumbers(?string $phone): array
+    {
+        $digits = preg_replace('/\D+/', '', (string) $phone) ?? '';
+
+        if (strlen($digits) < 10) {
+            return [[
+                'ddd'    => '11',
+                'number' => '999999999',
+                'type'   => 'mobile',
+            ]];
+        }
+
+        if (str_starts_with($digits, '55')) {
+            $digits = substr($digits, 2);
+        }
+
+        $areaCode = substr($digits, 0, 2);
+        $number   = substr($digits, 2);
+
+        return [[
+            'ddd'    => $areaCode,
+            'number' => $number,
+            'type'   => 'mobile',
+        ]];
+    }
+}