Просмотр исходного кода

refactor: passa validacoes para classes de app/Data/

Gustavo Mantovani 2 недель назад
Родитель
Сommit
d15bc5da41
41 измененных файлов с 962 добавлено и 943 удалено
  1. 26 0
      app/Data/Pagarme/PagarmeData.php
  2. 49 4
      app/Data/Pagarme/Request/PagarmeBankAccountUpdateRequestData.php
  3. 31 2
      app/Data/Pagarme/Request/PagarmeCardRequestData/PagarmeCardBillingAddressData.php
  4. 4 2
      app/Data/Pagarme/Request/PagarmeCardRequestData/PagarmeCardRequestData.php
  5. 19 1
      app/Data/Pagarme/Request/PagarmeCustomerRequestData/PagarmeCustomerAddressRequestData.php
  6. 6 2
      app/Data/Pagarme/Request/PagarmeCustomerRequestData/PagarmeCustomerPhonesRequestData/PagarmeCustomerPhoneData.php
  7. 27 1
      app/Data/Pagarme/Request/PagarmeCustomerRequestData/PagarmeCustomerPhonesRequestData/PagarmeCustomerPhonesRequestData.php
  8. 32 2
      app/Data/Pagarme/Request/PagarmeCustomerRequestData/PagarmeCustomerRequestData.php
  9. 10 2
      app/Data/Pagarme/Request/PagarmeCustomerRequestData/PagarmeCustomerUpdateRequestData.php
  10. 232 2
      app/Data/Pagarme/Request/PagarmeOrderRequestData/PagarmeOrderRequestData.php
  11. 1 1
      app/Data/Pagarme/Request/PagarmeRecipientRequestData/PagarmeRecipientAutomaticAnticipationSettingsData.php
  12. 49 4
      app/Data/Pagarme/Request/PagarmeRecipientRequestData/PagarmeRecipientBankAccountData.php
  13. 9 2
      app/Data/Pagarme/Request/PagarmeRecipientRequestData/PagarmeRecipientRegisterInformationData/PagarmeRecipientAddressData.php
  14. 6 2
      app/Data/Pagarme/Request/PagarmeRecipientRequestData/PagarmeRecipientRegisterInformationData/PagarmeRecipientPhoneNumbersData/PagarmeRecipientPhoneData.php
  15. 1 1
      app/Data/Pagarme/Request/PagarmeRecipientRequestData/PagarmeRecipientRegisterInformationData/PagarmeRecipientPhoneNumbersData/PagarmeRecipientPhoneNumbersData.php
  16. 9 2
      app/Data/Pagarme/Request/PagarmeRecipientRequestData/PagarmeRecipientRegisterInformationData/PagarmeRecipientRegisterInformationData.php
  17. 110 2
      app/Data/Pagarme/Request/PagarmeRecipientRequestData/PagarmeRecipientRequestData.php
  18. 8 2
      app/Data/Pagarme/Request/PagarmeRecipientRequestData/PagarmeRecipientTransferSettingsData.php
  19. 5 2
      app/Data/Pagarme/Request/PagarmeTransferRequestData.php
  20. 10 1
      app/Data/Pagarme/Response/PagarmeCardResponseData.php
  21. 1 1
      app/Data/Pagarme/Response/PagarmeCustomerResponseData/PagarmeCustomerAddressResponseData.php
  22. 1 1
      app/Data/Pagarme/Response/PagarmeCustomerResponseData/PagarmeCustomerPhonesResponseData/PagarmeCustomerPhonesResponseData.php
  23. 1 1
      app/Data/Pagarme/Response/PagarmeCustomerResponseData/PagarmeCustomerPhonesResponseData/PagarmePhoneResponseData.php
  24. 10 1
      app/Data/Pagarme/Response/PagarmeCustomerResponseData/PagarmeCustomerResponseData.php
  25. 128 1
      app/Data/Pagarme/Response/PagarmeOrderResponseData/PagarmeOrderResponseData.php
  26. 1 1
      app/Data/Pagarme/Response/PagarmeRecipientResponseData/PagarmeRecipientBankAccountResponseData.php
  27. 10 1
      app/Data/Pagarme/Response/PagarmeRecipientResponseData/PagarmeRecipientResponseData.php
  28. 12 12
      app/Data/Pagarme/Response/PagarmeTransferResponseData.php
  29. 1 11
      app/Http/Requests/PaymentSplitRequest.php
  30. 6 11
      app/Http/Resources/PaymentSplitResource.php
  31. 0 1
      app/Http/Resources/ProviderWithdrawalResource.php
  32. 0 32
      app/Models/PaymentSplit.php
  33. 1 0
      app/Services/Pagarme/Concerns/SendsPagarmeRequests.php
  34. 11 91
      app/Services/Pagarme/PagarmeCardService.php
  35. 7 199
      app/Services/Pagarme/PagarmeCustomerService.php
  36. 51 283
      app/Services/Pagarme/PagarmePaymentService.php
  37. 6 233
      app/Services/Pagarme/PagarmeRecipientService.php
  38. 4 3
      app/Services/PaymentService.php
  39. 52 5
      app/Services/ProviderWithdrawalService.php
  40. 15 9
      routes/authRoutes/provider.php
  41. 0 9
      routes/authRoutes/provider_withdrawal.php

+ 26 - 0
app/Data/Pagarme/PagarmeData.php

@@ -6,6 +6,11 @@ abstract readonly class PagarmeData
 {
     abstract public function toArray(): array;
 
+    protected static function digits(?string $value): string
+    {
+        return preg_replace('/\D+/', '', (string) $value) ?? '';
+    }
+
     protected function filterFilledRecursive(array $data): array
     {
         $filtered = [];
@@ -26,4 +31,25 @@ abstract readonly class PagarmeData
 
         return $filtered;
     }
+
+    protected static function requireFilled(mixed $value, string $field): void
+    {
+        if ($value === null || $value === '' || $value === []) {
+            throw new \InvalidArgumentException("{$field} e obrigatorio.");
+        }
+    }
+
+    protected static function requireIn(string $value, array $allowed, string $field): void
+    {
+        if (! in_array($value, $allowed, true)) {
+            throw new \InvalidArgumentException("{$field} invalido.");
+        }
+    }
+
+    protected static function requirePositiveInt(int $value, string $field): void
+    {
+        if ($value <= 0) {
+            throw new \InvalidArgumentException("{$field} deve ser maior que zero.");
+        }
+    }
 }

+ 49 - 4
app/Data/Pagarme/Request/PagarmeBankAccountUpdateRequestData.php

@@ -3,8 +3,9 @@
 namespace App\Data\Pagarme\Request;
 
 use App\Data\Pagarme\PagarmeData;
+use Illuminate\Support\Str;
 
-readonly class PagarmeBankAccountUpdateRequestData extends PagarmeData
+final readonly class PagarmeBankAccountUpdateRequestData extends PagarmeData
 {
     public function __construct(
         public string $holderName,
@@ -16,14 +17,23 @@ readonly class PagarmeBankAccountUpdateRequestData extends PagarmeData
         public string $accountNumber,
         public string $accountCheckDigit,
         public string $type,
-    ) {}
+    ) {
+        self::requireFilled($this->holderName, 'bank_account.holder_name');
+        self::requireIn($this->holderType, ['individual', 'company'], 'bank_account.holder_type');
+        self::requireFilled($this->holderDocument, 'bank_account.holder_document');
+        self::requireFilled($this->bank, 'bank_account.bank');
+        self::requireFilled($this->branchNumber, 'bank_account.branch_number');
+        self::requireFilled($this->accountNumber, 'bank_account.account_number');
+        self::requireFilled($this->accountCheckDigit, 'bank_account.account_check_digit');
+        self::requireFilled($this->type, 'bank_account.type');
+    }
 
     public static function fromArray(array $payload): self
     {
         return new self(
-            holderName: $payload['holder_name'],
+            holderName: self::normalizeHolderName($payload['holder_name']),
             holderType: $payload['holder_type'],
-            holderDocument: preg_replace('/\D+/', '', $payload['holder_document']),
+            holderDocument: self::digits($payload['holder_document']),
             bank: $payload['bank'],
             branchNumber: $payload['branch_number'],
             branchCheckDigit: $payload['branch_check_digit'] ?? null,
@@ -49,4 +59,39 @@ readonly class PagarmeBankAccountUpdateRequestData extends PagarmeData
             ],
         ]);
     }
+
+    private static function normalizeHolderName(string $holderName): string
+    {
+        $holderName = trim(preg_replace('/\s+/', ' ', $holderName) ?? '');
+
+        if (Str::length($holderName) < 30) {
+            return $holderName;
+        }
+
+        $parts = explode(' ', $holderName);
+
+        if (count($parts) >= 3) {
+            $firstName = array_shift($parts);
+            $lastName = array_pop($parts);
+
+            $initials = array_map(
+                static fn (string $part): string => Str::upper(Str::substr($part, 0, 1)),
+                $parts
+            );
+
+            $abbreviated = trim($firstName.' '.implode(' ', $initials).' '.$lastName);
+
+            if (Str::length($abbreviated) < 30) {
+                return $abbreviated;
+            }
+
+            $firstAndLast = trim($firstName.' '.$lastName);
+
+            if (Str::length($firstAndLast) < 30) {
+                return $firstAndLast;
+            }
+        }
+
+        return Str::limit($holderName, 29, '');
+    }
 }

+ 31 - 2
app/Data/Pagarme/Request/PagarmeCardRequestData/PagarmeCardBillingAddressData.php

@@ -3,8 +3,9 @@
 namespace App\Data\Pagarme\Request\PagarmeCardRequestData;
 
 use App\Data\Pagarme\PagarmeData;
+use App\Models\Address;
 
-readonly class PagarmeCardBillingAddressData extends PagarmeData
+final readonly class PagarmeCardBillingAddressData extends PagarmeData
 {
     public function __construct(
         public string $line1,
@@ -13,7 +14,35 @@ readonly class PagarmeCardBillingAddressData extends PagarmeData
         public string $city,
         public string $state,
         public string $country,
-    ) {}
+    ) {
+        self::requireFilled($this->line1, 'billing_address.line_1');
+        self::requireFilled($this->zipCode, 'billing_address.zip_code');
+        self::requireFilled($this->city, 'billing_address.city');
+        self::requireFilled($this->state, 'billing_address.state');
+        self::requireFilled($this->country, 'billing_address.country');
+    }
+
+    public static function fromAddress(?Address $address): self
+    {
+        $state = $address?->state?->code ?? $address?->city?->state?->code;
+
+        $city = $address?->city?->name;
+
+        $line1 = implode(', ', array_filter([
+            $address?->number ?: 'S/N',
+            $address?->address,
+            $address?->district,
+        ]));
+
+        return new self(
+            line1: $line1,
+            line2: $address?->complement ?: $address?->instructions,
+            zipCode: self::digits($address?->zip_code),
+            city: (string) $city,
+            state: (string) $state,
+            country: 'BR',
+        );
+    }
 
     public function toArray(): array
     {

+ 4 - 2
app/Data/Pagarme/Request/PagarmeCardRequestData/PagarmeCardRequestData.php

@@ -4,13 +4,15 @@ namespace App\Data\Pagarme\Request\PagarmeCardRequestData;
 
 use App\Data\Pagarme\PagarmeData;
 
-readonly class PagarmeCardRequestData extends PagarmeData
+final readonly class PagarmeCardRequestData extends PagarmeData
 {
     public function __construct(
         public string $token,
         public ?string $label = null,
         public ?PagarmeCardBillingAddressData $billingAddress = null,
-    ) {}
+    ) {
+        self::requireFilled($this->token, 'token');
+    }
 
     public function toArray(): array
     {

+ 19 - 1
app/Data/Pagarme/Request/PagarmeCustomerRequestData/PagarmeCustomerAddressRequestData.php

@@ -4,7 +4,7 @@ namespace App\Data\Pagarme\Request\PagarmeCustomerRequestData;
 
 use App\Data\Pagarme\PagarmeData;
 
-readonly class PagarmeCustomerAddressRequestData extends PagarmeData
+final readonly class PagarmeCustomerAddressRequestData extends PagarmeData
 {
     public function __construct(
         public ?string $line1,
@@ -15,6 +15,24 @@ readonly class PagarmeCustomerAddressRequestData extends PagarmeData
         public ?string $country,
     ) {}
 
+    public static function fromPayload(array $data): self
+    {
+        $line1Parts = array_filter([
+            (string) ($data['number'] ?? '0'),
+            (string) ($data['address'] ?? ''),
+            (string) ($data['district'] ?? ''),
+        ], static fn ($value) => $value !== '');
+
+        return new self(
+            line1: implode(', ', $line1Parts),
+            line2: (string) ($data['complement'] ?? $data['instructions'] ?? ''),
+            zipCode: self::digits($data['zip_code'] ?? null),
+            city: (string) ($data['city'] ?? ''),
+            state: (string) ($data['state'] ?? ''),
+            country: (string) ($data['country'] ?? 'BR'),
+        );
+    }
+
     public function toArray(): array
     {
         return $this->filterFilledRecursive([

+ 6 - 2
app/Data/Pagarme/Request/PagarmeCustomerRequestData/PagarmeCustomerPhonesRequestData/PagarmeCustomerPhoneData.php

@@ -4,13 +4,17 @@ namespace App\Data\Pagarme\Request\PagarmeCustomerRequestData\PagarmeCustomerPho
 
 use App\Data\Pagarme\PagarmeData;
 
-readonly class PagarmeCustomerPhoneData extends PagarmeData
+final readonly class PagarmeCustomerPhoneData extends PagarmeData
 {
     public function __construct(
         public string $countryCode,
         public string $areaCode,
         public string $number,
-    ) {}
+    ) {
+        self::requireFilled($this->countryCode, 'phone.country_code');
+        self::requireFilled($this->areaCode, 'phone.area_code');
+        self::requireFilled($this->number, 'phone.number');
+    }
 
     public function toArray(): array
     {

+ 27 - 1
app/Data/Pagarme/Request/PagarmeCustomerRequestData/PagarmeCustomerPhonesRequestData/PagarmeCustomerPhonesRequestData.php

@@ -4,13 +4,39 @@ namespace App\Data\Pagarme\Request\PagarmeCustomerRequestData\PagarmeCustomerPho
 
 use App\Data\Pagarme\PagarmeData;
 
-readonly class PagarmeCustomerPhonesRequestData extends PagarmeData
+final readonly class PagarmeCustomerPhonesRequestData extends PagarmeData
 {
     public function __construct(
         public ?PagarmeCustomerPhoneData $homePhone = null,
         public ?PagarmeCustomerPhoneData $mobilePhone = null,
     ) {}
 
+    public static function fromPhone(?string $phone): self
+    {
+        $digits = self::digits($phone);
+
+        if ($digits === '') {
+            return new self;
+        }
+
+        $areaCode = substr($digits, 0, 2);
+
+        $number = substr($digits, 2);
+
+        if (strlen($digits) <= 2) {
+            $areaCode = '';
+            $number = $digits;
+        }
+
+        return new self(
+            mobilePhone: new PagarmeCustomerPhoneData(
+                countryCode: '55',
+                areaCode: $areaCode,
+                number: $number,
+            ),
+        );
+    }
+
     public function toArray(): array
     {
         return $this->filterFilledRecursive([

+ 32 - 2
app/Data/Pagarme/Request/PagarmeCustomerRequestData/PagarmeCustomerRequestData.php

@@ -3,8 +3,9 @@
 namespace App\Data\Pagarme\Request\PagarmeCustomerRequestData;
 
 use App\Data\Pagarme\PagarmeData;
+use App\Data\Pagarme\Request\PagarmeCustomerRequestData\PagarmeCustomerPhonesRequestData\PagarmeCustomerPhonesRequestData;
 
-readonly class PagarmeCustomerRequestData extends PagarmeData
+final readonly class PagarmeCustomerRequestData extends PagarmeData
 {
     public function __construct(
         public string $name,
@@ -15,7 +16,36 @@ readonly class PagarmeCustomerRequestData extends PagarmeData
         public string $code,
         public ?PagarmeCustomerAddressRequestData $address = null,
         public ?PagarmeCustomerPhonesRequestData $phones = null,
-    ) {}
+    ) {
+        self::requireFilled($this->name, 'name');
+        self::requireFilled($this->email, 'email');
+        self::requireFilled($this->document, 'document');
+        self::requireIn($this->type, ['individual', 'company'], 'type');
+        self::requireIn($this->documentType, ['CPF', 'CNPJ'], 'document_type');
+        self::requireFilled($this->code, 'code');
+    }
+
+    public static function fromPayload(
+        ?string $name,
+        ?string $email,
+        ?string $document,
+        string $code,
+        array $addressData = [],
+        ?string $phone = null,
+    ): self {
+        $document = self::digits($document);
+
+        return new self(
+            name: $name ?: 'Cliente',
+            email: (string) $email,
+            document: $document,
+            type: strlen($document) === 14 ? 'company' : 'individual',
+            documentType: strlen($document) === 14 ? 'CNPJ' : 'CPF',
+            code: $code,
+            address: PagarmeCustomerAddressRequestData::fromPayload($addressData),
+            phones: PagarmeCustomerPhonesRequestData::fromPhone($phone),
+        );
+    }
 
     public function toArray(): array
     {

+ 10 - 2
app/Data/Pagarme/Request/PagarmeCustomerRequestData/PagarmeCustomerUpdateRequestData.php

@@ -4,7 +4,7 @@ namespace App\Data\Pagarme\Request\PagarmeCustomerRequestData;
 
 use App\Data\Pagarme\PagarmeData;
 
-readonly class PagarmeCustomerUpdateRequestData extends PagarmeData
+final readonly class PagarmeCustomerUpdateRequestData extends PagarmeData
 {
     public function __construct(
         public ?string $name = null,
@@ -15,7 +15,15 @@ readonly class PagarmeCustomerUpdateRequestData extends PagarmeData
         public ?string $code = null,
         public ?PagarmeCustomerAddressRequestData $address = null,
         public ?PagarmeCustomerPhonesRequestData $phones = null,
-    ) {}
+    ) {
+        if ($this->type !== null) {
+            self::requireIn($this->type, ['individual', 'company'], 'type');
+        }
+
+        if ($this->documentType !== null) {
+            self::requireIn($this->documentType, ['CPF', 'CNPJ'], 'document_type');
+        }
+    }
 
     public function toArray(): array
     {

+ 232 - 2
app/Data/Pagarme/Request/PagarmeOrderRequestData/PagarmeOrderRequestData.php

@@ -3,6 +3,8 @@
 namespace App\Data\Pagarme\Request\PagarmeOrderRequestData;
 
 use App\Data\Pagarme\PagarmeData;
+use App\Models\PaymentSplit;
+use Illuminate\Support\Collection;
 
 /**
  * @param  array<int, array{code?: string, amount?: int, quantity?: int, description?: string, ...}>  $items  Lista de itens do pedido
@@ -10,7 +12,7 @@ use App\Data\Pagarme\PagarmeData;
  * @param  array<string, mixed>  $metadata  Metadados do pedido (ex: payment_id, schedule_id, client_id, provider_id)
  * @param  array<string, mixed>|null  $customer  Dados do cliente (name, email, document, type, etc.)
  */
-readonly class PagarmeOrderRequestData extends PagarmeData
+final readonly class PagarmeOrderRequestData extends PagarmeData
 {
     public function __construct(
         public string $code,
@@ -21,7 +23,235 @@ readonly class PagarmeOrderRequestData extends PagarmeData
         public ?string $customerId = null,
         public bool $closed = true,
         public ?string $channel = null,
-    ) {}
+    ) {
+        self::requireFilled($this->code, 'code');
+
+        if (empty($this->items)) {
+            throw new \InvalidArgumentException('items nao pode estar vazio.');
+        }
+
+        if (empty($this->payments)) {
+            throw new \InvalidArgumentException('payments nao pode estar vazio.');
+        }
+
+        foreach ($this->payments as $index => $payment) {
+            if (! is_array($payment) || empty($payment['payment_method'])) {
+                throw new \InvalidArgumentException("payments.{$index}.payment_method e obrigatorio.");
+            }
+
+            self::requireIn($payment['payment_method'], ['credit_card', 'pix'], "payments.{$index}.payment_method");
+        }
+
+        if (! $this->customerId && empty($this->customer)) {
+            throw new \InvalidArgumentException('customer ou customer_id e obrigatorio.');
+        }
+    }
+
+    public static function fromOrderPayload(
+        string $code,
+        array $items,
+        array $customer,
+        array $paymentMethod,
+        array $metadata,
+        mixed $customerId = null,
+        bool $closed = true,
+        ?string $channel = null,
+    ): self {
+        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 = self::filled($customerId) ? (string) $customerId : null;
+
+        $customerPayload = self::filterFilledRecursiveStatic($customer);
+
+        if (! $customerIdPayload && empty($customerPayload)) {
+            throw new \InvalidArgumentException('customer ou customer_id e obrigatorio.');
+        }
+
+        return new self(
+            code: $code,
+            items: self::validateItems($items),
+            payments: [self::filterFilledRecursiveStatic($paymentMethod)],
+            metadata: $metadata,
+            customer: ! empty($customerPayload) ? $customerPayload : null,
+            customerId: $customerIdPayload,
+            closed: $closed,
+            channel: $channel,
+        );
+    }
+
+    //
+
+    public static function creditCardPaymentMethod(array $creditCard, ?array $split = null): array
+    {
+        return self::paymentMethodWithOptionalSplit([
+            'payment_method' => 'credit_card',
+            'credit_card'    => self::buildCreditCardPayload($creditCard),
+        ], $split);
+    }
+
+    public static function pixPaymentMethod(array $pix, ?array $split = null): array
+    {
+        return self::paymentMethodWithOptionalSplit([
+            'payment_method' => 'pix',
+            'pix'            => self::buildPixPayload($pix),
+        ], $split);
+    }
+
+    public static function splitFromTransfers(Collection $transfers): array
+    {
+        return $transfers
+            ->filter(fn (PaymentSplit $split) => ! empty($split->gateway_transfer_target_reference))
+            ->map(function (PaymentSplit $split) {
+                return [
+                    'amount'       => self::amountInCents((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();
+    }
+
+    //
+
+    private static 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) && self::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 static function buildPixPayload(array $pix): array
+    {
+        if (! self::filled($pix['expires_in'] ?? null) && ! self::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) && self::filled($pix[$field])) {
+                $payload[$field] = $pix[$field];
+            }
+        }
+
+        return $payload;
+    }
+
+    //
+
+    public static function amountInCents(float $amount): int
+    {
+        return (int) round($amount * 100);
+    }
+
+    private static function filled(mixed $value): bool
+    {
+        return $value !== null && $value !== '' && $value !== [];
+    }
+
+    private static function filterFilledRecursiveStatic(array $data): array
+    {
+        $filtered = [];
+
+        foreach ($data as $key => $value) {
+            if (is_array($value)) {
+                $value = self::filterFilledRecursiveStatic($value);
+            }
+
+            if (self::filled($value)) {
+                $filtered[$key] = $value;
+            }
+        }
+
+        return $filtered;
+    }
+
+    private static function paymentMethodWithOptionalSplit(array $paymentMethod, ?array $split): array
+    {
+        if (! empty($split)) {
+            $paymentMethod['split'] = $split;
+        }
+
+        return $paymentMethod;
+    }
+
+    private static 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) || ! self::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 self::filterFilledRecursiveStatic($item);
+            })
+            ->values()
+            ->all();
+    }
+
+    //
 
     public function toArray(): array
     {

+ 1 - 1
app/Data/Pagarme/Request/PagarmeRecipientRequestData/PagarmeRecipientAutomaticAnticipationSettingsData.php

@@ -4,7 +4,7 @@ namespace App\Data\Pagarme\Request\PagarmeRecipientRequestData;
 
 use App\Data\Pagarme\PagarmeData;
 
-readonly class PagarmeRecipientAutomaticAnticipationSettingsData extends PagarmeData
+final readonly class PagarmeRecipientAutomaticAnticipationSettingsData extends PagarmeData
 {
     public function __construct(
         public bool $enabled,

+ 49 - 4
app/Data/Pagarme/Request/PagarmeRecipientRequestData/PagarmeRecipientBankAccountData.php

@@ -3,8 +3,9 @@
 namespace App\Data\Pagarme\Request\PagarmeRecipientRequestData;
 
 use App\Data\Pagarme\PagarmeData;
+use Illuminate\Support\Str;
 
-readonly class PagarmeRecipientBankAccountData extends PagarmeData
+final readonly class PagarmeRecipientBankAccountData extends PagarmeData
 {
     public function __construct(
         public string $holderName,
@@ -16,14 +17,23 @@ readonly class PagarmeRecipientBankAccountData extends PagarmeData
         public string $accountNumber,
         public string $accountCheckDigit,
         public string $type,
-    ) {}
+    ) {
+        self::requireFilled($this->holderName, 'holder_name');
+        self::requireIn($this->holderType, ['individual', 'company'], 'holder_type');
+        self::requireFilled($this->holderDocument, 'holder_document');
+        self::requireFilled($this->bank, 'bank');
+        self::requireFilled($this->branchNumber, 'branch_number');
+        self::requireFilled($this->accountNumber, 'account_number');
+        self::requireFilled($this->accountCheckDigit, 'account_check_digit');
+        self::requireFilled($this->type, 'type');
+    }
 
     public static function fromArray(array $payload): self
     {
         return new self(
-            holderName: $payload['holder_name'],
+            holderName: self::normalizeHolderName($payload['holder_name']),
             holderType: $payload['holder_type'],
-            holderDocument: preg_replace('/\D+/', '', $payload['holder_document']),
+            holderDocument: self::digits($payload['holder_document']),
             bank: $payload['bank'],
             branchNumber: $payload['branch_number'],
             branchCheckDigit: $payload['branch_check_digit'] ?? null,
@@ -47,4 +57,39 @@ readonly class PagarmeRecipientBankAccountData extends PagarmeData
             'type'                => $this->type,
         ]);
     }
+
+    private static function normalizeHolderName(string $holderName): string
+    {
+        $holderName = trim(preg_replace('/\s+/', ' ', $holderName) ?? '');
+
+        if (Str::length($holderName) < 30) {
+            return $holderName;
+        }
+
+        $parts = explode(' ', $holderName);
+
+        if (count($parts) >= 3) {
+            $firstName = array_shift($parts);
+            $lastName = array_pop($parts);
+
+            $initials = array_map(
+                static fn (string $part): string => Str::upper(Str::substr($part, 0, 1)),
+                $parts
+            );
+
+            $abbreviated = trim($firstName.' '.implode(' ', $initials).' '.$lastName);
+
+            if (Str::length($abbreviated) < 30) {
+                return $abbreviated;
+            }
+
+            $firstAndLast = trim($firstName.' '.$lastName);
+
+            if (Str::length($firstAndLast) < 30) {
+                return $firstAndLast;
+            }
+        }
+
+        return Str::limit($holderName, 29, '');
+    }
 }

+ 9 - 2
app/Data/Pagarme/Request/PagarmeRecipientRequestData/PagarmeRecipientRegisterInformationData/PagarmeRecipientAddressData.php

@@ -4,7 +4,7 @@ namespace App\Data\Pagarme\Request\PagarmeRecipientRequestData\PagarmeRecipientR
 
 use App\Data\Pagarme\PagarmeData;
 
-readonly class PagarmeRecipientAddressData extends PagarmeData
+final readonly class PagarmeRecipientAddressData extends PagarmeData
 {
     public function __construct(
         public string $street,
@@ -15,7 +15,14 @@ readonly class PagarmeRecipientAddressData extends PagarmeData
         public ?string $state,
         public string $zipCode,
         public string $referencePoint,
-    ) {}
+    ) {
+        self::requireFilled($this->street, 'register_information.address.street');
+        self::requireFilled($this->streetNumber, 'register_information.address.street_number');
+        self::requireFilled($this->neighborhood, 'register_information.address.neighborhood');
+        self::requireFilled($this->city, 'register_information.address.city');
+        self::requireFilled($this->state, 'register_information.address.state');
+        self::requireFilled($this->zipCode, 'register_information.address.zip_code');
+    }
 
     public function toArray(): array
     {

+ 6 - 2
app/Data/Pagarme/Request/PagarmeRecipientRequestData/PagarmeRecipientRegisterInformationData/PagarmeRecipientPhoneNumbersData/PagarmeRecipientPhoneData.php

@@ -4,13 +4,17 @@ namespace App\Data\Pagarme\Request\PagarmeRecipientRequestData\PagarmeRecipientR
 
 use App\Data\Pagarme\PagarmeData;
 
-readonly class PagarmeRecipientPhoneData extends PagarmeData
+final readonly class PagarmeRecipientPhoneData extends PagarmeData
 {
     public function __construct(
         public string $ddd,
         public string $number,
         public string $type,
-    ) {}
+    ) {
+        self::requireFilled($this->ddd, 'phone.ddd');
+        self::requireFilled($this->number, 'phone.number');
+        self::requireIn($this->type, ['home', 'mobile'], 'phone.type');
+    }
 
     public function toArray(): array
     {

+ 1 - 1
app/Data/Pagarme/Request/PagarmeRecipientRequestData/PagarmeRecipientRegisterInformationData/PagarmeRecipientPhoneNumbersData/PagarmeRecipientPhoneNumbersData.php

@@ -4,7 +4,7 @@ namespace App\Data\Pagarme\Request\PagarmeRecipientRequestData\PagarmeRecipientR
 
 use App\Data\Pagarme\PagarmeData;
 
-readonly class PagarmeRecipientPhoneNumbersData extends PagarmeData
+final readonly class PagarmeRecipientPhoneNumbersData extends PagarmeData
 {
     public function __construct(
         public PagarmeRecipientPhoneData $phone,

+ 9 - 2
app/Data/Pagarme/Request/PagarmeRecipientRequestData/PagarmeRecipientRegisterInformationData/PagarmeRecipientRegisterInformationData.php

@@ -5,7 +5,7 @@ namespace App\Data\Pagarme\Request\PagarmeRecipientRequestData\PagarmeRecipientR
 use App\Data\Pagarme\PagarmeData;
 use App\Data\Pagarme\Request\PagarmeRecipientRequestData\PagarmeRecipientRegisterInformationData\PagarmeRecipientPhoneNumbersData\PagarmeRecipientPhoneNumbersData;
 
-readonly class PagarmeRecipientRegisterInformationData extends PagarmeData
+final readonly class PagarmeRecipientRegisterInformationData extends PagarmeData
 {
     public function __construct(
         public string $name,
@@ -17,7 +17,14 @@ readonly class PagarmeRecipientRegisterInformationData extends PagarmeData
         public string $professionalOccupation,
         public PagarmeRecipientPhoneNumbersData $phoneNumbers,
         public PagarmeRecipientAddressData $address,
-    ) {}
+    ) {
+        self::requireFilled($this->name, 'register_information.name');
+        self::requireFilled($this->email, 'register_information.email');
+        self::requireFilled($this->document, 'register_information.document');
+        self::requireIn($this->type, ['individual', 'corporation', 'company'], 'register_information.type');
+        self::requirePositiveInt($this->monthlyIncome, 'register_information.monthly_income');
+        self::requireFilled($this->professionalOccupation, 'register_information.professional_occupation');
+    }
 
     public function toArray(): array
     {

+ 110 - 2
app/Data/Pagarme/Request/PagarmeRecipientRequestData/PagarmeRecipientRequestData.php

@@ -3,9 +3,13 @@
 namespace App\Data\Pagarme\Request\PagarmeRecipientRequestData;
 
 use App\Data\Pagarme\PagarmeData;
+use App\Data\Pagarme\Request\PagarmeRecipientRequestData\PagarmeRecipientRegisterInformationData\PagarmeRecipientAddressData;
+use App\Data\Pagarme\Request\PagarmeRecipientRequestData\PagarmeRecipientRegisterInformationData\PagarmeRecipientPhoneNumbersData\PagarmeRecipientPhoneData;
+use App\Data\Pagarme\Request\PagarmeRecipientRequestData\PagarmeRecipientRegisterInformationData\PagarmeRecipientPhoneNumbersData\PagarmeRecipientPhoneNumbersData;
 use App\Data\Pagarme\Request\PagarmeRecipientRequestData\PagarmeRecipientRegisterInformationData\PagarmeRecipientRegisterInformationData;
+use Carbon\Carbon;
 
-readonly class PagarmeRecipientRequestData extends PagarmeData
+final readonly class PagarmeRecipientRequestData extends PagarmeData
 {
     public function __construct(
         public string $code,
@@ -13,7 +17,47 @@ readonly class PagarmeRecipientRequestData extends PagarmeData
         public PagarmeRecipientBankAccountData $defaultBankAccount,
         public PagarmeRecipientTransferSettingsData $transferSettings,
         public PagarmeRecipientAutomaticAnticipationSettingsData $automaticAnticipationSettings,
-    ) {}
+    ) {
+        self::requireFilled($this->code, 'code');
+    }
+
+    public static function fromPayload(string $code, array $data): self
+    {
+        $addressParts = self::extractAddressParts($data);
+
+        return new self(
+            code: $code,
+            registerInformation: new PagarmeRecipientRegisterInformationData(
+                name: $data['recipient_name'],
+                email: $data['recipient_email'],
+                document: self::digits($data['recipient_document'] ?? null),
+                type: $data['recipient_type'] ?? 'individual',
+                birthdate: self::formatBirthdate($data['birth_date'] ?? null),
+                monthlyIncome: isset($data['monthly_income']) ? (int) $data['monthly_income'] : 1000,
+                professionalOccupation: $data['professional_occupation'] ?? 'autonomo',
+                phoneNumbers: new PagarmeRecipientPhoneNumbersData(self::phoneFromPayload($data['phone'] ?? null)),
+                address: new PagarmeRecipientAddressData(
+                    street: $data['address'],
+                    complementary: $addressParts['complementary'],
+                    streetNumber: $addressParts['street_number'],
+                    neighborhood: $addressParts['neighborhood'],
+                    city: $data['city'] ?? null,
+                    state: $data['state'] ?? null,
+                    zipCode: self::digits($data['zip_code'] ?? null),
+                    referencePoint: $addressParts['reference_point'],
+                ),
+            ),
+            defaultBankAccount: PagarmeRecipientBankAccountData::fromArray($data['recipient_default_bank_account']),
+            transferSettings: new PagarmeRecipientTransferSettingsData(
+                transferEnabled: false,
+                transferInterval: 'Daily',
+                transferDay: 0,
+            ),
+            automaticAnticipationSettings: new PagarmeRecipientAutomaticAnticipationSettingsData(
+                enabled: false,
+            ),
+        );
+    }
 
     public function toArray(): array
     {
@@ -25,4 +69,68 @@ readonly class PagarmeRecipientRequestData extends PagarmeData
             'automatic_anticipation_settings' => $this->automaticAnticipationSettings,
         ]);
     }
+
+    private static function phoneFromPayload(?string $phone): PagarmeRecipientPhoneData
+    {
+        $digits = self::digits($phone);
+
+        if (strlen($digits) < 10) {
+            return new PagarmeRecipientPhoneData(
+                ddd: '11',
+                number: '999999999',
+                type: 'mobile',
+            );
+        }
+
+        if (str_starts_with($digits, '55')) {
+            $digits = substr($digits, 2);
+        }
+
+        return new PagarmeRecipientPhoneData(
+            ddd: substr($digits, 0, 2),
+            number: substr($digits, 2),
+            type: 'mobile',
+        );
+    }
+
+    private static function extractAddressParts(array $data): array
+    {
+        $addressLine = trim((string) ($data['address'] ?? ''));
+        $segments = array_map('trim', explode(',', $addressLine));
+        $streetSegment = $segments[0] ?? '';
+
+        if (($data['number'] ?? null) === null) {
+            preg_match('/^(\d+)/', $streetSegment, $matches);
+        }
+
+        return [
+            'street_number'   => (string) ($data['number'] ?? $matches[1] ?? 'S/N'),
+            'neighborhood'    => (string) ($data['district'] ?? $segments[1] ?? 'N/A'),
+            'reference_point' => (string) ($data['reference_point'] ?? 'N/A'),
+            'complementary'   => (string) ($data['complement'] ?? 'N/A'),
+        ];
+    }
+
+    private static function formatBirthdate(mixed $birthdate): ?string
+    {
+        if ($birthdate === null || $birthdate === '') {
+            return null;
+        }
+
+        if ($birthdate instanceof \DateTimeInterface) {
+            return Carbon::instance($birthdate)->format('d/m/Y');
+        }
+
+        $birthdate = trim((string) $birthdate);
+
+        if (preg_match('/^\d{2}\/\d{2}\/\d{4}$/', $birthdate) === 1) {
+            return $birthdate;
+        }
+
+        if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $birthdate) === 1) {
+            return Carbon::createFromFormat('Y-m-d', $birthdate)->format('d/m/Y');
+        }
+
+        return Carbon::parse($birthdate)->format('d/m/Y');
+    }
 }

+ 8 - 2
app/Data/Pagarme/Request/PagarmeRecipientRequestData/PagarmeRecipientTransferSettingsData.php

@@ -4,13 +4,19 @@ namespace App\Data\Pagarme\Request\PagarmeRecipientRequestData;
 
 use App\Data\Pagarme\PagarmeData;
 
-readonly class PagarmeRecipientTransferSettingsData extends PagarmeData
+final readonly class PagarmeRecipientTransferSettingsData extends PagarmeData
 {
     public function __construct(
         public bool $transferEnabled,
         public string $transferInterval,
         public int $transferDay,
-    ) {}
+    ) {
+        self::requireIn($this->transferInterval, ['Daily', 'Weekly', 'Monthly'], 'transfer_interval');
+
+        if ($this->transferDay < 0) {
+            throw new \InvalidArgumentException('transfer_day deve ser maior ou igual a zero.');
+        }
+    }
 
     public function toArray(): array
     {

+ 5 - 2
app/Data/Pagarme/Request/PagarmeTransferRequestData.php

@@ -4,13 +4,16 @@ namespace App\Data\Pagarme\Request;
 
 use App\Data\Pagarme\PagarmeData;
 
-readonly class PagarmeTransferRequestData extends PagarmeData
+final readonly class PagarmeTransferRequestData extends PagarmeData
 {
     public function __construct(
         public int $amount,
         public string $recipientId,
         public ?array $metadata = null,
-    ) {}
+    ) {
+        self::requirePositiveInt($this->amount, 'amount');
+        self::requireFilled($this->recipientId, 'recipient_id');
+    }
 
     public function toArray(): array
     {

+ 10 - 1
app/Data/Pagarme/Response/PagarmeCardResponseData.php

@@ -2,7 +2,7 @@
 
 namespace App\Data\Pagarme\Response;
 
-readonly class PagarmeCardResponseData
+final readonly class PagarmeCardResponseData
 {
     public function __construct(
         public ?string $id,
@@ -40,6 +40,15 @@ readonly class PagarmeCardResponseData
         return $this->id;
     }
 
+    public function requireId(): string
+    {
+        if (! $this->id) {
+            throw new \RuntimeException('Pagar.me card creation returned an empty id.');
+        }
+
+        return $this->id;
+    }
+
     public function brand(): ?string
     {
         return $this->brand;

+ 1 - 1
app/Data/Pagarme/Response/PagarmeCustomerResponseData/PagarmeCustomerAddressResponseData.php

@@ -2,7 +2,7 @@
 
 namespace App\Data\Pagarme\Response\PagarmeCustomerResponseData;
 
-readonly class PagarmeCustomerAddressResponseData
+final readonly class PagarmeCustomerAddressResponseData
 {
     public function __construct(
         public ?string $id,

+ 1 - 1
app/Data/Pagarme/Response/PagarmeCustomerResponseData/PagarmeCustomerPhonesResponseData/PagarmeCustomerPhonesResponseData.php

@@ -2,7 +2,7 @@
 
 namespace App\Data\Pagarme\Response\PagarmeCustomerResponseData\PagarmeCustomerPhonesResponseData;
 
-readonly class PagarmeCustomerPhonesResponseData
+final readonly class PagarmeCustomerPhonesResponseData
 {
     public function __construct(
         public ?PagarmePhoneResponseData $homePhone,

+ 1 - 1
app/Data/Pagarme/Response/PagarmeCustomerResponseData/PagarmeCustomerPhonesResponseData/PagarmePhoneResponseData.php

@@ -2,7 +2,7 @@
 
 namespace App\Data\Pagarme\Response\PagarmeCustomerResponseData\PagarmeCustomerPhonesResponseData;
 
-readonly class PagarmePhoneResponseData
+final readonly class PagarmePhoneResponseData
 {
     public function __construct(
         public ?string $countryCode,

+ 10 - 1
app/Data/Pagarme/Response/PagarmeCustomerResponseData/PagarmeCustomerResponseData.php

@@ -4,7 +4,7 @@ namespace App\Data\Pagarme\Response\PagarmeCustomerResponseData;
 
 use App\Data\Pagarme\Response\PagarmeCustomerResponseData\PagarmeCustomerPhonesResponseData\PagarmeCustomerPhonesResponseData;
 
-readonly class PagarmeCustomerResponseData
+final readonly class PagarmeCustomerResponseData
 {
     public function __construct(
         public ?string $id,
@@ -44,6 +44,15 @@ readonly class PagarmeCustomerResponseData
         return $this->id;
     }
 
+    public function requireId(): string
+    {
+        if (! $this->id) {
+            throw new \RuntimeException('Customer creation returned an empty id.');
+        }
+
+        return $this->id;
+    }
+
     public function toArray(): array
     {
         return [

+ 128 - 1
app/Data/Pagarme/Response/PagarmeOrderResponseData/PagarmeOrderResponseData.php

@@ -2,6 +2,8 @@
 
 namespace App\Data\Pagarme\Response\PagarmeOrderResponseData;
 
+use App\Enums\PaymentStatusEnum;
+
 /**
  * @param  array<int, array>  $items  Itens do pedido (code, amount, quantity, description, etc.)
  * @param  array<string, mixed>|null  $customer  Dados do cliente retornados pela API
@@ -9,7 +11,7 @@ namespace App\Data\Pagarme\Response\PagarmeOrderResponseData;
  * @param  array<int, array>  $checkouts  Checkouts do pedido
  * @param  array<string, mixed>  $metadata  Metadados retornados pela API
  */
-readonly class PagarmeOrderResponseData
+final readonly class PagarmeOrderResponseData
 {
     public function __construct(
         public ?string $id,
@@ -53,6 +55,122 @@ readonly class PagarmeOrderResponseData
         return $this->id;
     }
 
+    public function requireId(): string
+    {
+        if (! $this->id) {
+            throw new \RuntimeException('Pagar.me order creation returned an empty id.');
+        }
+
+        return $this->id;
+    }
+
+    public function firstCharge(): array
+    {
+        return $this->charges[0] ?? [];
+    }
+
+    public function lastTransaction(): array
+    {
+        return $this->firstCharge()['last_transaction'] ?? [];
+    }
+
+    public function gatewayEntityReference(): ?string
+    {
+        $charge = $this->firstCharge();
+
+        return $charge['id'] ?? $this->id;
+    }
+
+    public function gatewayEntityLabel(): string
+    {
+        return isset($this->firstCharge()['id']) ? 'charge' : 'order';
+    }
+
+    public function gatewayOperationReference(): ?string
+    {
+        $charge = $this->firstCharge();
+        $transaction = $this->lastTransaction();
+
+        return $transaction['id'] ?? $charge['id'] ?? $this->id;
+    }
+
+    public function gatewayOperationLabel(): string
+    {
+        $charge = $this->firstCharge();
+        $transaction = $this->lastTransaction();
+
+        return isset($transaction['id']) ? 'transaction' : (isset($charge['id']) ? 'charge' : 'order');
+    }
+
+    public function paymentStatus(): PaymentStatusEnum
+    {
+        $charge = $this->firstCharge();
+        $transaction = $this->lastTransaction();
+        $status = strtolower((string) (($transaction['status'] ?? null) ?: ($charge['status'] ?? null)));
+
+        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,
+        };
+    }
+
+    public function paidAt(): ?string
+    {
+        return $this->filledArrayValue($this->firstCharge(), 'paid_at');
+    }
+
+    public function authorizedAt(): ?string
+    {
+        $transaction = $this->lastTransaction();
+        $transactionStatus = $transaction['status'] ?? null;
+
+        if (in_array($transactionStatus, ['authorized_pending_capture', 'captured', 'partial_capture'], true)) {
+            return $this->filledArrayValue($transaction, 'created_at');
+        }
+
+        return null;
+    }
+
+    public function failureCode(): ?string
+    {
+        return $this->filledArrayValue($this->lastTransaction()['gateway_response'] ?? [], 'code');
+    }
+
+    public function failureMessage(): ?string
+    {
+        $transaction = $this->lastTransaction();
+        $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;
+
+        if ($message && str_contains($message, 'Sem ambiente configurado')) {
+            return 'Pix não esta habilitado ou configurado neste ambiente do Pagar.me.';
+        }
+
+        return $message;
+    }
+
     public function toArray(): array
     {
         return [
@@ -72,4 +190,13 @@ readonly class PagarmeOrderResponseData
             'metadata'   => $this->metadata,
         ];
     }
+
+    private function filledArrayValue(array $data, string $field): ?string
+    {
+        if (! array_key_exists($field, $data) || $data[$field] === null || $data[$field] === '' || $data[$field] === []) {
+            return null;
+        }
+
+        return (string) $data[$field];
+    }
 }

+ 1 - 1
app/Data/Pagarme/Response/PagarmeRecipientResponseData/PagarmeRecipientBankAccountResponseData.php

@@ -2,7 +2,7 @@
 
 namespace App\Data\Pagarme\Response\PagarmeRecipientResponseData;
 
-readonly class PagarmeRecipientBankAccountResponseData
+final readonly class PagarmeRecipientBankAccountResponseData
 {
     public function __construct(
         public ?string $holderName,

+ 10 - 1
app/Data/Pagarme/Response/PagarmeRecipientResponseData/PagarmeRecipientResponseData.php

@@ -2,7 +2,7 @@
 
 namespace App\Data\Pagarme\Response\PagarmeRecipientResponseData;
 
-readonly class PagarmeRecipientResponseData
+final readonly class PagarmeRecipientResponseData
 {
     public function __construct(
         public ?string $id,
@@ -38,6 +38,15 @@ readonly class PagarmeRecipientResponseData
         return $this->id;
     }
 
+    public function requireId(): string
+    {
+        if (! $this->id) {
+            throw new \RuntimeException('Pagar.me recipient creation returned an empty id.');
+        }
+
+        return $this->id;
+    }
+
     public function defaultBankAccount(): ?PagarmeRecipientBankAccountResponseData
     {
         return $this->defaultBankAccount;

+ 12 - 12
app/Data/Pagarme/Response/PagarmeTransferResponseData.php

@@ -2,7 +2,7 @@
 
 namespace App\Data\Pagarme\Response;
 
-readonly class PagarmeTransferResponseData
+final readonly class PagarmeTransferResponseData
 {
     public function __construct(
         public ?string $id,
@@ -48,17 +48,17 @@ readonly class PagarmeTransferResponseData
     public function toArray(): array
     {
         return [
-            'id'                      => $this->id,
-            'amount'                  => $this->amount,
-            'type'                    => $this->type,
-            'status'                  => $this->status,
-            'fee'                     => $this->fee,
-            'funding_date'            => $this->fundingDate,
-            'funding_estimated_date'  => $this->fundingEstimatedDate,
-            'bank_account'            => $this->bankAccount,
-            'bank_response'           => $this->bankResponse,
-            'created_at'              => $this->createdAt,
-            'metadata'                => $this->metadata,
+            'id'                     => $this->id,
+            'amount'                 => $this->amount,
+            'type'                   => $this->type,
+            'status'                 => $this->status,
+            'fee'                    => $this->fee,
+            'funding_date'           => $this->fundingDate,
+            'funding_estimated_date' => $this->fundingEstimatedDate,
+            'bank_account'           => $this->bankAccount,
+            'bank_response'          => $this->bankResponse,
+            'created_at'             => $this->createdAt,
+            'metadata'               => $this->metadata,
         ];
     }
 }

+ 1 - 11
app/Http/Requests/PaymentSplitRequest.php

@@ -18,12 +18,6 @@ class PaymentSplitRequest extends FormRequest
             'provider_id' => ['sometimes', 'integer', 'exists:providers,id'],
 
             'gateway_provider'                  => ['sometimes', 'string', 'max:255'],
-            'gateway_entity_reference'          => ['nullable', 'string', 'max:255'],
-            'gateway_entity_label'              => ['nullable', 'string', 'max:255'],
-            'gateway_operation_reference'       => ['nullable', 'string', 'max:255'],
-            'gateway_operation_label'           => ['nullable', 'string', 'max:255'],
-            'gateway_parent_reference'          => ['nullable', 'string', 'max:255'],
-            'gateway_parent_label'              => ['nullable', 'string', 'max:255'],
             'gateway_transfer_target_reference' => ['nullable', 'string', 'max:255'],
             'gateway_transfer_target_label'     => ['nullable', 'string', 'max:255'],
 
@@ -34,12 +28,8 @@ class PaymentSplitRequest extends FormRequest
             'net_amount'         => ['sometimes', 'numeric', 'min:0'],
 
             'transferred_at' => ['nullable', 'date'],
-            'failed_at'      => ['nullable', 'date'],
 
-            'failure_code'    => ['nullable', 'string', 'max:255'],
-            'failure_message' => ['nullable', 'string'],
-            'gateway_payload' => ['nullable', 'array'],
-            'metadata'        => ['nullable', 'array'],
+            'metadata' => ['nullable', 'array'],
         ];
 
         if ($this->isMethod('POST')) {

+ 6 - 11
app/Http/Resources/PaymentSplitResource.php

@@ -2,7 +2,6 @@
 
 namespace App\Http\Resources;
 
-use App\Models\PaymentSplit;
 use Illuminate\Http\Request;
 use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
 use Illuminate\Http\Resources\Json\JsonResource;
@@ -16,12 +15,6 @@ class PaymentSplitResource extends JsonResource
             'payment_id'                        => $this->payment_id,
             'provider_id'                       => $this->provider_id,
             'gateway_provider'                  => $this->gateway_provider,
-            'gateway_entity_reference'          => $this->gateway_entity_reference,
-            'gateway_entity_label'              => $this->gateway_entity_label,
-            'gateway_operation_reference'       => $this->gateway_operation_reference,
-            'gateway_operation_label'           => $this->gateway_operation_label,
-            'gateway_parent_reference'          => $this->gateway_parent_reference,
-            'gateway_parent_label'              => $this->gateway_parent_label,
             'gateway_transfer_target_reference' => $this->gateway_transfer_target_reference,
             'gateway_transfer_target_label'     => $this->gateway_transfer_target_label,
             'status'                            => $this->status,
@@ -29,11 +22,13 @@ class PaymentSplitResource extends JsonResource
             'gateway_fee_amount'                => $this->gateway_fee_amount,
             'net_amount'                        => $this->net_amount,
             'transferred_at'                    => $this->transferred_at?->toISOString(),
-            'failed_at'                         => $this->failed_at?->toISOString(),
-            'failure_code'                      => $this->failure_code,
-            'failure_message'                   => $this->failure_message,
-            'gateway_payload'                   => $this->gateway_payload,
             'metadata'                          => $this->metadata,
+            'payment_status'                    => $this->payment?->status?->value,
+            'payment_paid_at'                   => $this->payment?->paid_at?->toISOString(),
+            'schedule_date'                     => $this->payment?->schedule?->date,
+            'schedule_status'                   => $this->payment?->schedule?->status,
+            'schedule_period_type'              => $this->payment?->schedule?->period_type,
+            'client_name'                       => $this->payment?->schedule?->client?->user?->name,
             'created_at'                        => $this->created_at?->toISOString(),
             'updated_at'                        => $this->updated_at?->toISOString(),
         ];

+ 0 - 1
app/Http/Resources/ProviderWithdrawalResource.php

@@ -2,7 +2,6 @@
 
 namespace App\Http\Resources;
 
-use App\Models\ProviderWithdrawal;
 use Illuminate\Http\Request;
 use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
 use Illuminate\Http\Resources\Json\JsonResource;

+ 0 - 32
app/Models/PaymentSplit.php

@@ -11,12 +11,6 @@ use Illuminate\Database\Eloquent\SoftDeletes;
  * @property int $payment_id
  * @property int $provider_id
  * @property string $gateway_provider
- * @property string|null $gateway_entity_reference
- * @property string|null $gateway_entity_label
- * @property string|null $gateway_operation_reference
- * @property string|null $gateway_operation_label
- * @property string|null $gateway_parent_reference
- * @property string|null $gateway_parent_label
  * @property string|null $gateway_transfer_target_reference
  * @property string|null $gateway_transfer_target_label
  * @property \App\Enums\PaymentSplitStatusEnum $status
@@ -24,10 +18,6 @@ use Illuminate\Database\Eloquent\SoftDeletes;
  * @property numeric $gateway_fee_amount Custo do gateway associado ao repasse.
  * @property numeric $net_amount Valor liquido do repasse: net_amount = gross_amount - gateway_fee_amount.
  * @property \Illuminate\Support\Carbon|null $transferred_at
- * @property \Illuminate\Support\Carbon|null $failed_at
- * @property string|null $failure_code
- * @property string|null $failure_message
- * @property array<array-key, mixed>|null $gateway_payload
  * @property array<array-key, mixed>|null $metadata
  * @property \Illuminate\Support\Carbon|null $created_at
  * @property \Illuminate\Support\Carbon|null $updated_at
@@ -42,17 +32,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
  * @method static \Illuminate\Database\Eloquent\Builder<static>|PaymentSplit query()
  * @method static \Illuminate\Database\Eloquent\Builder<static>|PaymentSplit whereCreatedAt($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|PaymentSplit whereDeletedAt($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|PaymentSplit whereFailedAt($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|PaymentSplit whereFailureCode($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|PaymentSplit whereFailureMessage($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|PaymentSplit whereGatewayEntityLabel($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|PaymentSplit whereGatewayEntityReference($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|PaymentSplit whereGatewayFeeAmount($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|PaymentSplit whereGatewayOperationLabel($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|PaymentSplit whereGatewayOperationReference($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|PaymentSplit whereGatewayParentLabel($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|PaymentSplit whereGatewayParentReference($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|PaymentSplit whereGatewayPayload($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|PaymentSplit whereGatewayProvider($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|PaymentSplit whereGatewayTransferTargetLabel($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|PaymentSplit whereGatewayTransferTargetReference($value)
@@ -81,12 +61,6 @@ class PaymentSplit extends Model
         'provider_id',
         'provider_withdrawal_id',
         'gateway_provider',
-        'gateway_entity_reference',
-        'gateway_entity_label',
-        'gateway_operation_reference',
-        'gateway_operation_label',
-        'gateway_parent_reference',
-        'gateway_parent_label',
         'gateway_transfer_target_reference',
         'gateway_transfer_target_label',
         'status',
@@ -94,10 +68,6 @@ class PaymentSplit extends Model
         'gateway_fee_amount',
         'net_amount',
         'transferred_at',
-        'failed_at',
-        'failure_code',
-        'failure_message',
-        'gateway_payload',
         'metadata',
     ];
 
@@ -107,8 +77,6 @@ class PaymentSplit extends Model
         'net_amount'         => 'decimal:2',
         'status'             => \App\Enums\PaymentSplitStatusEnum::class,
         'transferred_at'     => 'datetime',
-        'failed_at'          => 'datetime',
-        'gateway_payload'    => 'array',
         'metadata'           => 'array',
         'created_at'         => 'datetime',
         'updated_at'         => 'datetime',

+ 1 - 0
app/Services/Pagarme/Concerns/SendsPagarmeRequests.php

@@ -17,6 +17,7 @@ trait SendsPagarmeRequests
         array|PagarmeData $payload,
     ): array {
         $payload = $payload instanceof PagarmeData ? $payload->toArray() : $payload;
+
         $endpoint = $this->pagarmeUrl($path);
 
         try {

+ 11 - 91
app/Services/Pagarme/PagarmeCardService.php

@@ -8,7 +8,6 @@ use App\Data\Pagarme\Response\PagarmeCardResponseData;
 use App\Models\Address;
 use App\Models\ClientPaymentMethod;
 use App\Services\Pagarme\Concerns\SendsPagarmeRequests;
-use Illuminate\Support\Facades\Log;
 
 class PagarmeCardService
 {
@@ -24,10 +23,6 @@ class PagarmeCardService
             return $paymentMethod->gateway_card_id;
         }
 
-        if (empty($paymentMethod->token)) {
-            throw new \InvalidArgumentException('Token do cartao e obrigatorio para salvar cartao no Pagar.me.');
-        }
-
         $paymentMethod->loadMissing('client.user');
 
         $customerId = $this->pagarmeCustomerService->createCustomerForClient(
@@ -35,10 +30,6 @@ class PagarmeCardService
             []
         );
 
-        if (empty($customerId)) {
-            throw new \RuntimeException('Cliente precisa ter customer_id do Pagar.me para salvar cartao.');
-        }
-
         $cardData = PagarmeCardResponseData::fromArray($this->pagarmeRequest(
             method: 'POST',
             path: "/customers/{$customerId}/cards",
@@ -46,25 +37,22 @@ class PagarmeCardService
             payload: new PagarmeCardRequestData(
                 token: $paymentMethod->token,
                 label: $paymentMethod->card_name,
-                billingAddress: $this->buildBillingAddress($paymentMethod),
+                billingAddress: PagarmeCardBillingAddressData::fromAddress(
+                    Address::query()
+                        ->with(['city.state', 'state'])
+                        ->where('source', 'client')
+                        ->where('source_id', $paymentMethod->client_id)
+                        ->orderByDesc('is_primary')
+                        ->latest('id')
+                        ->first()
+                ),
             ),
 
             idempotencyKey: $this->idempotencyKey($paymentMethod),
             errorMessage: 'Erro ao salvar cartao no Pagar.me.',
         ));
 
-        $cardId = $cardData->id();
-
-        if (empty($cardId)) {
-            Log::channel('pagarme')->error('Pagar.me card creation returned empty id', [
-                'client_payment_method_id' => $paymentMethod->id,
-                'client_id'                => $paymentMethod->client_id,
-                'customer_id'              => $customerId,
-                'response'                 => $cardData->toArray(),
-            ]);
-
-            throw new \RuntimeException('Pagar.me card creation returned an empty id.');
-        }
+        $cardId = $cardData->requireId();
 
         $paymentMethod->forceFill([
             'gateway_card_id'  => $cardId,
@@ -75,78 +63,10 @@ class PagarmeCardService
         return $cardId;
     }
 
-    //
+    // evita criacao duplicada de cartao em retries/reprocessamentos
 
     private function idempotencyKey(ClientPaymentMethod $paymentMethod): string
     {
         return "client-payment-method-{$paymentMethod->id}-card";
     }
-
-    //
-
-    private function buildBillingAddress(ClientPaymentMethod $paymentMethod): PagarmeCardBillingAddressData
-    {
-        $address = Address::query()
-            ->with(['city.state', 'state'])
-            ->where('source', 'client')
-            ->where('source_id', $paymentMethod->client_id)
-            ->orderByDesc('is_primary')
-            ->latest('id')
-            ->first();
-
-        if (! $address) {
-            throw new \InvalidArgumentException('Cliente precisa ter endereco para salvar cartao no Pagar.me.');
-        }
-
-        $state = $address->state?->code ?? $address->city?->state?->code;
-        $city = $address->city?->name;
-
-        $line1 = implode(', ', array_filter([
-            $address->number ?: 'S/N',
-            $address->address,
-            $address->district,
-        ]));
-
-        foreach ([
-            'estado'   => $state,
-            'cidade'   => $city,
-            'cep'      => $this->digits($address->zip_code),
-            'endereco' => $line1,
-        ] as $field => $value) {
-            if ($value === null || $value === '') {
-                throw new \InvalidArgumentException("Cliente precisa ter {$field} valido para salvar cartao no Pagar.me.");
-            }
-        }
-
-        return new PagarmeCardBillingAddressData(
-            line1: $line1,
-            line2: $address->complement ?: $address->instructions,
-            zipCode: $this->digits($address->zip_code),
-            city: $city,
-            state: $state,
-            country: 'BR',
-        );
-    }
-
-    private function digits(?string $value): string
-    {
-        return preg_replace('/\D+/', '', (string) $value) ?? '';
-    }
-
-    private function filterFilledRecursive(array $data): array
-    {
-        $filtered = [];
-
-        foreach ($data as $key => $value) {
-            if (is_array($value)) {
-                $value = $this->filterFilledRecursive($value);
-            }
-
-            if ($value !== null && $value !== '' && $value !== []) {
-                $filtered[$key] = $value;
-            }
-        }
-
-        return $filtered;
-    }
 }

+ 7 - 199
app/Services/Pagarme/PagarmeCustomerService.php

@@ -2,15 +2,10 @@
 
 namespace App\Services\Pagarme;
 
-use App\Data\Pagarme\Request\PagarmeCustomerRequestData\PagarmeCustomerAddressRequestData;
-use App\Data\Pagarme\Request\PagarmeCustomerRequestData\PagarmeCustomerPhonesRequestData\PagarmeCustomerPhoneData;
-use App\Data\Pagarme\Request\PagarmeCustomerRequestData\PagarmeCustomerPhonesRequestData\PagarmeCustomerPhonesRequestData;
 use App\Data\Pagarme\Request\PagarmeCustomerRequestData\PagarmeCustomerRequestData;
-use App\Data\Pagarme\Request\PagarmeCustomerRequestData\PagarmeCustomerUpdateRequestData;
 use App\Data\Pagarme\Response\PagarmeCustomerResponseData\PagarmeCustomerResponseData;
 use App\Models\Client;
 use App\Services\Pagarme\Concerns\SendsPagarmeRequests;
-use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Str;
 
 class PagarmeCustomerService
@@ -28,52 +23,24 @@ class PagarmeCustomerService
         $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)) {
-            Log::channel('pagarme')->warning(
-                'Skipping customer creation because the client is missing email or document.',
-                [
-                    'client_id' => $client->id,
-                    'user_id'   => $client->user_id,
-                ]
-            );
-
-            return null;
-        }
-
-        $address = $this->buildAddress($data);
-        $phones = $this->buildPhones($client->user?->phone ?? $data['phone'] ?? null);
-
         $code = $this->ensureCustomerCode($client);
 
         $customerData = PagarmeCustomerResponseData::fromArray($this->pagarmeRequest(
             method: 'POST',
             path: '/customers',
-            payload: new PagarmeCustomerRequestData(
+            payload: PagarmeCustomerRequestData::fromPayload(
                 name: $name,
                 email: $email,
-                document: $document,
-                type: $this->personType($document),
-                documentType: $this->documentType($document),
+                document: $client->document ?? $data['document'] ?? null,
                 code: $code,
-                address: $address,
-                phones: $phones,
+                addressData: $data,
+                phone: $client->user?->phone ?? $data['phone'] ?? null,
             ),
             idempotencyKey: $this->idempotencyKey($client->id),
             errorMessage: 'Erro ao criar cliente no Pagar.me.',
         ));
 
-        $customerId = $customerData->id();
-
-        if (! $customerId) {
-            Log::channel('pagarme')->error('Customer creation returned an empty id.', [
-                'client_id' => $client->id,
-                'response'  => $customerData->toArray(),
-            ]);
-
-            throw new \RuntimeException('Customer creation returned an empty id.');
-        }
+        $customerId = $customerData->requireId();
 
         $client->forceFill([
             'external_customer_id'   => $customerId,
@@ -83,31 +50,7 @@ class PagarmeCustomerService
         return $customerId;
     }
 
-    public function updateCustomer(string $customerId, int $clientId, array $data): void
-    {
-        $payload = new PagarmeCustomerUpdateRequestData(
-            name: $data['name'] ?? null,
-            email: $data['email'] ?? null,
-            document: $this->sanitizeDigits($data['document'] ?? null),
-            type: $data['type'] ?? null,
-            documentType: $data['document_type'] ?? null,
-            code: $data['code'] ?? null,
-            address: $this->addressFromPayload($data['address'] ?? null),
-            phones: $this->phonesFromPayload($data['phones'] ?? null),
-        );
-
-        if (empty($payload->toArray())) {
-            return;
-        }
-
-        $this->pagarmeRequest(
-            method: 'PATCH',
-            path: "/customers/{$customerId}",
-            payload: $payload,
-            idempotencyKey: $this->idempotencyKey($clientId, "customer-update-{$customerId}"),
-            errorMessage: 'Erro ao atualizar cliente no Pagar.me.',
-        );
-    }
+    // evita criacao duplicada de customer
 
     private function idempotencyKey(int $clientId, string $suffix = 'customer'): string
     {
@@ -116,7 +59,7 @@ class PagarmeCustomerService
 
     private function ensureCustomerCode(Client $client): string
     {
-        if ($this->hasUuidCode($client->external_customer_code, 'client')) {
+        if (! empty($client->external_customer_code)) {
             return $client->external_customer_code;
         }
 
@@ -126,139 +69,4 @@ class PagarmeCustomerService
 
         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;
-    }
-
-    private function buildAddress(array $data): PagarmeCustomerAddressRequestData
-    {
-        $zipCode = $this->sanitizeDigits($data['zip_code'] ?? null);
-
-        $street = (string) ($data['address'] ?? '');
-        $number = (string) ($data['number'] ?? '0');
-        $neighborhood = (string) ($data['district'] ?? '');
-
-        $city = (string) ($data['city'] ?? '');
-        $state = (string) ($data['state'] ?? '');
-        $country = (string) ($data['country'] ?? 'BR');
-
-        $complement = (string) ($data['complement'] ?? '');
-
-        $line1Parts = array_filter([$number, $street, $neighborhood], static fn ($value) => $value !== '');
-
-        $line1 = implode(', ', $line1Parts);
-        $line2 = $complement ?: (string) ($data['instructions'] ?? '');
-
-        return new PagarmeCustomerAddressRequestData(
-            line1: $line1,
-            line2: $line2,
-            zipCode: $zipCode,
-            city: $city,
-            state: $state,
-            country: $country,
-        );
-    }
-
-    private function buildPhones(?string $phone): PagarmeCustomerPhonesRequestData
-    {
-        $digits = $this->sanitizeDigits($phone);
-
-        if (empty($digits)) {
-            return new PagarmeCustomerPhonesRequestData;
-        }
-
-        $countryCode = '55';
-
-        $areaCode = substr($digits, 0, 2);
-        $number = substr($digits, 2);
-
-        if (strlen($digits) <= 2) {
-            $areaCode = '';
-            $number = $digits;
-        }
-
-        return new PagarmeCustomerPhonesRequestData(
-            mobilePhone: new PagarmeCustomerPhoneData(
-                countryCode: $countryCode,
-                areaCode: $areaCode,
-                number: $number,
-            ),
-        );
-    }
-
-    private function addressFromPayload(mixed $address): ?PagarmeCustomerAddressRequestData
-    {
-        if (! is_array($address) || empty($address)) {
-            return null;
-        }
-
-        return new PagarmeCustomerAddressRequestData(
-            line1: $address['line_1'] ?? null,
-            line2: $address['line_2'] ?? null,
-            zipCode: $address['zip_code'] ?? null,
-            city: $address['city'] ?? null,
-            state: $address['state'] ?? null,
-            country: $address['country'] ?? null,
-        );
-    }
-
-    private function phonesFromPayload(mixed $phones): ?PagarmeCustomerPhonesRequestData
-    {
-        if (! is_array($phones) || empty($phones)) {
-            return null;
-        }
-
-        return new PagarmeCustomerPhonesRequestData(
-            homePhone: $this->phoneFromPayload($phones['home_phone'] ?? null),
-            mobilePhone: $this->phoneFromPayload($phones['mobile_phone'] ?? null),
-        );
-    }
-
-    private function phoneFromPayload(mixed $phone): ?PagarmeCustomerPhoneData
-    {
-        if (! is_array($phone) || empty($phone)) {
-            return null;
-        }
-
-        return new PagarmeCustomerPhoneData(
-            countryCode: (string) ($phone['country_code'] ?? ''),
-            areaCode: (string) ($phone['area_code'] ?? ''),
-            number: (string) ($phone['number'] ?? ''),
-        );
-    }
-
-    private function documentType(string $document): string
-    {
-        return strlen($document) === 14 ? 'CNPJ' : 'CPF';
-    }
-
-    private function filterFilledRecursive(array $data): array
-    {
-        $filtered = [];
-
-        foreach ($data as $key => $value) {
-            if (is_array($value)) {
-                $value = $this->filterFilledRecursive($value);
-            }
-
-            if ($value !== null && $value !== '' && $value !== []) {
-                $filtered[$key] = $value;
-            }
-        }
-
-        return $filtered;
-    }
-
-    private function personType(string $document): string
-    {
-        return strlen($document) === 14 ? 'company' : 'individual';
-    }
-
-    private function sanitizeDigits(?string $value): string
-    {
-        return preg_replace('/\D+/', '', (string) $value) ?? '';
-    }
 }

+ 51 - 283
app/Services/Pagarme/PagarmePaymentService.php

@@ -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();
     }
 }

+ 6 - 233
app/Services/Pagarme/PagarmeRecipientService.php

@@ -3,19 +3,10 @@
 namespace App\Services\Pagarme;
 
 use App\Data\Pagarme\Request\PagarmeBankAccountUpdateRequestData;
-use App\Data\Pagarme\Request\PagarmeRecipientRequestData\PagarmeRecipientAutomaticAnticipationSettingsData;
-use App\Data\Pagarme\Request\PagarmeRecipientRequestData\PagarmeRecipientBankAccountData;
-use App\Data\Pagarme\Request\PagarmeRecipientRequestData\PagarmeRecipientRegisterInformationData\PagarmeRecipientAddressData;
-use App\Data\Pagarme\Request\PagarmeRecipientRequestData\PagarmeRecipientRegisterInformationData\PagarmeRecipientPhoneNumbersData\PagarmeRecipientPhoneData;
-use App\Data\Pagarme\Request\PagarmeRecipientRequestData\PagarmeRecipientRegisterInformationData\PagarmeRecipientPhoneNumbersData\PagarmeRecipientPhoneNumbersData;
-use App\Data\Pagarme\Request\PagarmeRecipientRequestData\PagarmeRecipientRegisterInformationData\PagarmeRecipientRegisterInformationData;
 use App\Data\Pagarme\Request\PagarmeRecipientRequestData\PagarmeRecipientRequestData;
-use App\Data\Pagarme\Request\PagarmeRecipientRequestData\PagarmeRecipientTransferSettingsData;
 use App\Data\Pagarme\Response\PagarmeRecipientResponseData\PagarmeRecipientResponseData;
 use App\Models\Provider;
 use App\Services\Pagarme\Concerns\SendsPagarmeRequests;
-use Carbon\Carbon;
-use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Str;
 
 class PagarmeRecipientService
@@ -28,52 +19,11 @@ class PagarmeRecipientService
             return $provider->recipient_id;
         }
 
-        $bankAccountData = $data['recipient_default_bank_account'];
-        $bankAccountData['holder_name'] = $this->normalizeBankAccountHolderName($bankAccountData['holder_name']);
         $metadata = $data['recipient_metadata'] ?? [];
         $paymentMode = $data['recipient_payment_mode'];
-        $recipientType = $data['recipient_type'] ?? 'individual';
-
-        $addressParts = $this->extractAddressParts($data);
-        $monthlyIncome = isset($data['monthly_income']) ? (int) $data['monthly_income'] : 1000;
-        $occupation = $data['professional_occupation'] ?? 'autonomo';
-
         $recipientCode = $this->ensureRecipientCode($provider);
-
-        $defaultBankAccount = PagarmeRecipientBankAccountData::fromArray($bankAccountData);
-
-        $payload = new PagarmeRecipientRequestData(
-            code: $recipientCode,
-            registerInformation: new PagarmeRecipientRegisterInformationData(
-                name: $data['recipient_name'],
-                email: $data['recipient_email'],
-                document: preg_replace('/\D+/', '', $data['recipient_document']),
-                type: $recipientType,
-                birthdate: $this->formatBirthdate($data['birth_date'] ?? null),
-                monthlyIncome: $monthlyIncome,
-                professionalOccupation: $occupation,
-                phoneNumbers: new PagarmeRecipientPhoneNumbersData($this->buildPhoneNumber($data['phone'] ?? null)),
-                address: new PagarmeRecipientAddressData(
-                    street: $data['address'],
-                    complementary: $addressParts['complementary'],
-                    streetNumber: $addressParts['street_number'],
-                    neighborhood: $addressParts['neighborhood'],
-                    city: $data['city'] ?? null,
-                    state: $data['state'] ?? null,
-                    zipCode: preg_replace('/\D+/', '', $data['zip_code']),
-                    referencePoint: $addressParts['reference_point'],
-                ),
-            ),
-            defaultBankAccount: $defaultBankAccount,
-            transferSettings: new PagarmeRecipientTransferSettingsData(
-                transferEnabled: false,
-                transferInterval: 'Daily',
-                transferDay: 0,
-            ),
-            automaticAnticipationSettings: new PagarmeRecipientAutomaticAnticipationSettingsData(
-                enabled: false,
-            ),
-        );
+        $payload = PagarmeRecipientRequestData::fromPayload($recipientCode, $data);
+        $bankAccountData = $payload->defaultBankAccount->toArray();
 
         $recipientData = PagarmeRecipientResponseData::fromArray($this->pagarmeRequest(
             method: 'POST',
@@ -83,15 +33,7 @@ class PagarmeRecipientService
             errorMessage: 'Erro ao criar recebedor no Pagar.me.',
         ));
 
-        $recipientId = $recipientData->id();
-
-        if (! $recipientId) {
-            Log::channel('pagarme')->error('Pagar.me recipient creation returned empty id', [
-                'response' => $recipientData->toArray(),
-            ]);
-
-            throw new \RuntimeException('Pagar.me recipient creation returned an empty id.');
-        }
+        $recipientId = $recipientData->requireId();
 
         $provider->forceFill([
             'recipient_id'                   => $recipientId,
@@ -99,7 +41,7 @@ class PagarmeRecipientService
             'recipient_email'                => $data['recipient_email'],
             'recipient_description'          => $data['recipient_description'],
             'recipient_document'             => $data['recipient_document'],
-            'recipient_type'                 => $recipientType,
+            'recipient_type'                 => $payload->registerInformation->type,
             'recipient_code'                 => $recipientCode,
             'recipient_payment_mode'         => $paymentMode,
             'recipient_default_bank_account' => $bankAccountData,
@@ -122,13 +64,6 @@ class PagarmeRecipientService
 
     public function updateDefaultBankAccount(Provider $provider, array $bankAccountData): Provider
     {
-        if (empty($provider->recipient_id)) {
-            throw new \InvalidArgumentException('Prestador precisa ter recipient_id do Pagar.me para atualizar a conta bancaria.');
-        }
-
-        $bankAccountData = $this->normalizeBankAccountPayload($bankAccountData);
-        $bankAccountData['holder_name'] = $this->normalizeBankAccountHolderName($bankAccountData['holder_name']);
-
         $payload = PagarmeBankAccountUpdateRequestData::fromArray($bankAccountData);
 
         $recipientData = PagarmeRecipientResponseData::fromArray($this->pagarmeRequest(
@@ -140,7 +75,7 @@ class PagarmeRecipientService
         ));
 
         $provider->forceFill([
-            'recipient_default_bank_account' => $recipientData->defaultBankAccount()?->toArray() ?: $bankAccountData,
+            'recipient_default_bank_account' => $recipientData->defaultBankAccount()?->toArray() ?: $payload->toArray()['bank_account'],
         ])->save();
 
         return $provider->fresh();
@@ -153,36 +88,9 @@ class PagarmeRecipientService
         return "provider-{$providerId}-{$suffix}";
     }
 
-    private function buildPhoneNumber(?string $phone): PagarmeRecipientPhoneData
-    {
-        $digits = preg_replace('/\D+/', '', (string) $phone) ?? '';
-
-        if (strlen($digits) < 10) {
-            return new PagarmeRecipientPhoneData(
-                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 new PagarmeRecipientPhoneData(
-            ddd: $areaCode,
-            number: $number,
-            type: 'mobile',
-        );
-    }
-
     private function ensureRecipientCode(Provider $provider): string
     {
-        if ($this->hasUuidCode($provider->recipient_code, 'provider')) {
+        if (! empty($provider->recipient_code)) {
             return $provider->recipient_code;
         }
 
@@ -192,139 +100,4 @@ class PagarmeRecipientService
 
         return $recipientCode;
     }
-
-    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;
-    }
-
-    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 filterFilledRecursive(array $data): array
-    {
-        $filtered = [];
-
-        foreach ($data as $key => $value) {
-            if (is_array($value)) {
-                $value = $this->filterFilledRecursive($value);
-            }
-
-            if ($value !== null && $value !== '' && $value !== []) {
-                $filtered[$key] = $value;
-            }
-        }
-
-        return $filtered;
-    }
-
-    private function formatBirthdate(mixed $birthdate): ?string
-    {
-        if ($birthdate === null || $birthdate === '') {
-            return null;
-        }
-
-        if ($birthdate instanceof \DateTimeInterface) {
-            return Carbon::instance($birthdate)->format('d/m/Y');
-        }
-
-        $birthdate = trim((string) $birthdate);
-
-        if (preg_match('/^\d{2}\/\d{2}\/\d{4}$/', $birthdate) === 1) {
-            return $birthdate;
-        }
-
-        if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $birthdate) === 1) {
-            return Carbon::createFromFormat('Y-m-d', $birthdate)->format('d/m/Y');
-        }
-
-        return Carbon::parse($birthdate)->format('d/m/Y');
-    }
-
-    private function normalizeBankAccountHolderName(string $holderName): string
-    {
-        $holderName = trim(preg_replace('/\s+/', ' ', $holderName) ?? '');
-
-        if (Str::length($holderName) < 30) {
-            return $holderName;
-        }
-
-        $parts = explode(' ', $holderName);
-
-        if (count($parts) >= 3) {
-            $firstName = array_shift($parts);
-            $lastName = array_pop($parts);
-
-            $initials = array_map(
-                static fn (string $part): string => Str::upper(Str::substr($part, 0, 1)),
-                $parts
-            );
-
-            $abbreviated = trim($firstName.' '.implode(' ', $initials).' '.$lastName);
-
-            if (Str::length($abbreviated) < 30) {
-                return $abbreviated;
-            }
-
-            $firstAndLast = trim($firstName.' '.$lastName);
-
-            if (Str::length($firstAndLast) < 30) {
-                return $firstAndLast;
-            }
-        }
-
-        return Str::limit($holderName, 29, '');
-    }
-
-    private function normalizeBankAccountPayload(array $bankAccountData): array
-    {
-        return [
-            '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'],
-            'metadata'            => $bankAccountData['metadata'] ?? null,
-        ];
-    }
 }

+ 4 - 3
app/Services/PaymentService.php

@@ -2,6 +2,7 @@
 
 namespace App\Services;
 
+use App\Data\Pagarme\Request\PagarmeOrderRequestData\PagarmeOrderRequestData;
 use App\Enums\PaymentSplitStatusEnum;
 use App\Enums\PaymentStatusEnum;
 use App\Models\Address;
@@ -201,11 +202,11 @@ class PaymentService
             ],
         ]);
 
-        $split = $this->pagarmePaymentService->buildSplitFromTransfers(collect([$transfer]));
+        $split = PagarmeOrderRequestData::splitFromTransfers(collect([$transfer]));
 
         if ($platformFee > 0) {
             $split[] = [
-                'amount'       => $this->pagarmePaymentService->toGatewayAmountInCents($platformFee),
+                'amount'       => PagarmeOrderRequestData::amountInCents($platformFee),
                 'recipient_id' => $platformRecipientId,
                 'type'         => 'flat',
 
@@ -282,7 +283,7 @@ class PaymentService
             ?? "Servico {$schedule->id}";
 
         return [[
-            'amount'      => $this->pagarmePaymentService->toGatewayAmountInCents($grossAmount),
+            'amount'      => PagarmeOrderRequestData::amountInCents($grossAmount),
             'description' => $description,
             'quantity'    => 1,
             'code'        => "schedule-{$schedule->id}",

+ 52 - 5
app/Services/ProviderWithdrawalService.php

@@ -9,6 +9,7 @@ use App\Models\PaymentSplit;
 use App\Models\Provider;
 use App\Models\ProviderWithdrawal;
 use App\Services\Pagarme\PagarmeTransferService;
+use Carbon\Carbon;
 use Illuminate\Database\Eloquent\Collection;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Log;
@@ -24,7 +25,7 @@ class ProviderWithdrawalService
         $earnings = (float) PaymentSplit::query()
             ->where('provider_id', $provider->id)
             ->whereHas('payment', fn ($q) => $q->where('status', PaymentStatusEnum::PAID))
-            ->whereHas('payment.schedule', fn ($q) => $q->where('status', 'finished'))
+            ->whereHas('payment.schedule', fn ($q) => $this->availableScheduleQuery($q))
             ->sum('gross_amount');
 
         $withdrawn = (float) ProviderWithdrawal::query()
@@ -44,10 +45,19 @@ class ProviderWithdrawalService
         return (float) PaymentSplit::query()
             ->where('provider_id', $provider->id)
             ->whereHas('payment', fn ($q) => $q->where('status', PaymentStatusEnum::PAID))
-            ->whereHas('payment.schedule', fn ($q) => $q->whereNotIn('status', ['finished', 'cancelled', 'rejected']))
+            ->whereHas('payment.schedule', fn ($q) => $this->pendingScheduleQuery($q))
             ->sum('gross_amount');
     }
 
+    public function getPaymentSplits(Provider $provider): Collection
+    {
+        return PaymentSplit::query()
+            ->where('provider_id', $provider->id)
+            ->with(['payment.schedule.client.user'])
+            ->orderBy('created_at', 'desc')
+            ->get();
+    }
+
     public function requestWithdrawal(Provider $provider): ProviderWithdrawal
     {
         if (empty($provider->recipient_id)) {
@@ -120,14 +130,14 @@ class ProviderWithdrawalService
                     ->where('provider_id', $provider->id)
                     ->whereNull('provider_withdrawal_id')
                     ->whereHas('payment', fn ($q) => $q->where('status', PaymentStatusEnum::PAID))
-                    ->whereHas('payment.schedule', fn ($q) => $q->where('status', 'finished'))
+                    ->whereHas('payment.schedule', fn ($q) => $this->availableScheduleQuery($q))
                     ->update(['provider_withdrawal_id' => $withdrawal->id]);
 
                 return $withdrawal->fresh();
             } catch (\Throwable $e) {
                 $withdrawal->forceFill([
-                    'status'       => ProviderWithdrawalStatusEnum::FAILED,
-                    'failed_at'    => now(),
+                    'status'        => ProviderWithdrawalStatusEnum::FAILED,
+                    'failed_at'     => now(),
                     'bank_response' => $e->getMessage(),
                 ])->save();
 
@@ -194,4 +204,41 @@ class ProviderWithdrawalService
             ])->save();
         }
     }
+
+    private function availableScheduleQuery($query)
+    {
+        return $query
+            ->where('code_verified', true)
+            ->whereRaw(
+                $this->scheduleEndedAtExpression().' <= ?',
+                [$this->withdrawalReleaseCutoff()]
+            );
+    }
+
+    private function pendingScheduleQuery($query)
+    {
+        return $query
+            ->whereNotIn('status', ['cancelled', 'rejected'])
+            ->where(function ($q) {
+                $q->where('code_verified', false)
+                    ->orWhereRaw(
+                        $this->scheduleEndedAtExpression().' > ?',
+                        [$this->withdrawalReleaseCutoff()]
+                    );
+            });
+    }
+
+    private function scheduleEndedAtExpression(): string
+    {
+        return match (DB::connection()->getDriverName()) {
+            'pgsql'  => '(date + end_time)',
+            'sqlite' => "datetime(date || ' ' || end_time)",
+            default  => 'TIMESTAMP(date, end_time)',
+        };
+    }
+
+    private function withdrawalReleaseCutoff(): string
+    {
+        return Carbon::now()->subDays(5)->format('Y-m-d H:i:s');
+    }
 }

+ 15 - 9
routes/authRoutes/provider.php

@@ -1,14 +1,20 @@
 <?php
 
 use App\Http\Controllers\ProviderController;
+use App\Http\Controllers\ProviderWithdrawalController;
 use Illuminate\Support\Facades\Route;
 
-Route::get('/provider/pending',        [ProviderController::class, 'pending'])->middleware('permission:config.provider,view');
-Route::patch('/provider/{id}/approve', [ProviderController::class, 'approve'])->middleware('permission:config.provider,edit');
-Route::patch('/provider/{id}/reject',  [ProviderController::class, 'reject'])->middleware('permission:config.provider,edit');
-Route::get('/provider',                [ProviderController::class, 'index'])->middleware('permission:config.provider,view');
-Route::post('/provider',               [ProviderController::class, 'store'])->middleware('permission:config.provider,add');
-Route::get('/provider/{id}',           [ProviderController::class, 'show'])->middleware('permission:config.provider,view');
-Route::put('/provider/{id}',           [ProviderController::class, 'update'])->middleware('permission:config.provider,edit');
-Route::patch('/provider/{id}/bank-account', [ProviderController::class, 'updateBankAccount'])->middleware('permission:config.provider,edit');
-Route::delete('/provider/{id}',        [ProviderController::class, 'destroy'])->middleware('permission:config.provider,delete');
+Route::get('/provider/pending',                  [ProviderController::class, 'pending'])->middleware('permission:config.provider,view');
+Route::patch('/provider/{id}/approve',            [ProviderController::class, 'approve'])->middleware('permission:config.provider,edit');
+Route::patch('/provider/{id}/reject',             [ProviderController::class, 'reject'])->middleware('permission:config.provider,edit');
+Route::get('/provider/payment-splits',            [ProviderWithdrawalController::class, 'splits'])->middleware('permission:config.provider,view');
+Route::get('/provider/withdrawals/balance',       [ProviderWithdrawalController::class, 'balance'])->middleware('permission:config.provider,view');
+Route::get('/provider/withdrawals',               [ProviderWithdrawalController::class, 'index'])->middleware('permission:config.provider,view');
+Route::post('/provider/withdrawals',              [ProviderWithdrawalController::class, 'store'])->middleware('permission:config.provider,edit');
+Route::get('/provider/withdrawals/{id}',  [ProviderWithdrawalController::class, 'show'])->middleware('permission:config.provider,view');
+Route::get('/provider',                           [ProviderController::class, 'index'])->middleware('permission:config.provider,view');
+Route::post('/provider',                          [ProviderController::class, 'store'])->middleware('permission:config.provider,add');
+Route::get('/provider/{id}',                      [ProviderController::class, 'show'])->middleware('permission:config.provider,view');
+Route::put('/provider/{id}',                      [ProviderController::class, 'update'])->middleware('permission:config.provider,edit');
+Route::patch('/provider/{id}/bank-account',       [ProviderController::class, 'updateBankAccount'])->middleware('permission:config.provider,edit');
+Route::delete('/provider/{id}',                   [ProviderController::class, 'destroy'])->middleware('permission:config.provider,delete');

+ 0 - 9
routes/authRoutes/provider_withdrawal.php

@@ -1,9 +0,0 @@
-<?php
-
-use Illuminate\Support\Facades\Route;
-use App\Http\Controllers\ProviderWithdrawalController;
-
-Route::get('/provider/withdrawals/balance', [ProviderWithdrawalController::class, 'balance'])->middleware('permission:config.provider,view');
-Route::get('/provider/withdrawals', [ProviderWithdrawalController::class, 'index'])->middleware('permission:config.provider,view');
-Route::post('/provider/withdrawals', [ProviderWithdrawalController::class, 'store'])->middleware('permission:config.provider,edit');
-Route::get('/provider/withdrawals/{id}', [ProviderWithdrawalController::class, 'show'])->middleware('permission:config.provider,view');