Bläddra i källkod

Merge branch 'development' of gogs.softpar.inf.br:Softpar/sfp_api_laravel_diarista into development

Gustavo Zanatta 6 dagar sedan
förälder
incheckning
d913196b4b
57 ändrade filer med 1058 tillägg och 518 borttagningar
  1. 0 44
      _ide_helper.php
  2. 1 0
      app/Data/Pagarme/Request/CardRequestData/CardRequestData.php
  3. 1 0
      app/Data/Pagarme/Request/CustomerRequestData/CustomerRequestData.php
  4. 6 1
      app/Data/Pagarme/Request/OrderRequestData/OrderPaymentData/OrderPaymentData.php
  5. 1 1
      app/Data/Pagarme/Request/OrderRequestData/OrderPaymentData/OrderPixData/OrderPixAdditionalInformationData.php
  6. 1 1
      app/Data/Pagarme/Request/OrderRequestData/OrderPaymentData/OrderPixData/OrderPixData.php
  7. 2 1
      app/Data/Pagarme/Request/OrderRequestData/OrderPaymentData/OrderSplitData/OrderSplitData.php
  8. 1 1
      app/Data/Pagarme/Request/OrderRequestData/OrderPaymentData/OrderSplitData/OrderSplitOptionsData.php
  9. 4 3
      app/Data/Pagarme/Request/OrderRequestData/OrderRequestData.php
  10. 3 1
      app/Data/Pagarme/Request/RecipientRequestData/RecipientRegisterInformationData/RecipientRegisterInformationData.php
  11. 5 3
      app/Data/Pagarme/Request/RecipientRequestData/RecipientRequestData.php
  12. 2 0
      app/Data/Pagarme/Response/CardResponseData.php
  13. 4 0
      app/Data/Pagarme/Response/OrderResponseData/OrderChargeResponseData/OrderChargeResponseData.php
  14. 14 2
      app/Data/Pagarme/Response/OrderResponseData/OrderChargeResponseData/OrderTransactionResponseData.php
  15. 60 3
      app/Data/Pagarme/Response/OrderResponseData/OrderResponseData.php
  16. 2 0
      app/Data/Pagarme/Response/TransferResponseData.php
  17. 11 0
      app/Http/Controllers/PaymentController.php
  18. 7 2
      app/Http/Requests/AddressRequest.php
  19. 7 2
      app/Http/Requests/CityRequest.php
  20. 5 2
      app/Http/Requests/ClientFavoriteProviderRequest.php
  21. 4 2
      app/Http/Requests/ClientProviderBlockRequest.php
  22. 6 0
      app/Http/Requests/ClientRequest.php
  23. 4 2
      app/Http/Requests/ProviderClientBlockRequest.php
  24. 23 13
      app/Http/Requests/ProviderRequest.php
  25. 12 10
      app/Http/Requests/RegisterProviderRequest.php
  26. 2 1
      app/Http/Requests/ReviewImprovementRequest.php
  27. 3 1
      app/Http/Requests/ReviewRequest.php
  28. 2 0
      app/Http/Requests/StateRequest.php
  29. 1 0
      app/Http/Requests/UserAppsValidateCodeRequest.php
  30. 4 1
      app/Http/Requests/UserRequest.php
  31. 7 2
      app/Http/Resources/PaymentResource.php
  32. 25 6
      app/Models/Client.php
  33. 3 0
      app/Models/ClientPaymentMethod.php
  34. 9 2
      app/Models/DeviceToken.php
  35. 34 0
      app/Models/Notification.php
  36. 18 0
      app/Models/Payment.php
  37. 29 12
      app/Models/Provider.php
  38. 9 2
      app/Models/PushNotificationLog.php
  39. 2 2
      app/Models/Review.php
  40. 12 0
      app/Models/ReviewMedia.php
  41. 18 2
      app/Models/Schedule.php
  42. 4 0
      app/Models/User.php
  43. 91 0
      app/Services/Pagarme/Concerns/FormatsPagarmeData.php
  44. 32 9
      app/Services/Pagarme/Concerns/SendsPagarmeRequests.php
  45. 12 3
      app/Services/Pagarme/PagarmeCardService.php
  46. 39 47
      app/Services/Pagarme/PagarmeCustomerService.php
  47. 156 117
      app/Services/Pagarme/PagarmePaymentService.php
  48. 64 150
      app/Services/Pagarme/PagarmeRecipientService.php
  49. 63 44
      app/Services/Pagarme/PagarmeTransferService.php
  50. 79 1
      app/Services/PaymentService.php
  51. 41 15
      app/Services/ProviderService.php
  52. 1 1
      app/Services/PushNotificationService.php
  53. 46 0
      database/migrations/2026_06_03_163406_add_idempotency_key_to_related_tables.php
  54. 24 0
      database/migrations/2026_06_03_163929_rename_external_customer_columns_to_gateway_prefix_on_clients_table.php
  55. 3 2
      pint.json
  56. 5 2
      routes/authRoutes/payment.php
  57. 34 2
      routes/console.php

+ 0 - 44
_ide_helper.php

@@ -22782,49 +22782,6 @@ namespace Illuminate\Support\Facades {
             }
     }
 
-namespace Kreait\Laravel\Firebase\Facades {
-    /**
-     * @method static AppCheck appCheck()
-     * @method static Auth auth()
-     * @method static Database database()
-     * @method static Firestore firestore()
-     * @method static Messaging messaging()
-     * @method static RemoteConfig remoteConfig()
-     * @method static Storage storage()
-     * @see FirebaseProjectManager
-     * @see FirebaseProject
-     */
-    class Firebase {
-        /**
-         * @static
-         */
-        public static function project($name = null)
-        {
-            /** @var \Kreait\Laravel\Firebase\FirebaseProjectManager $instance */
-            return $instance->project($name);
-        }
-
-        /**
-         * @static
-         */
-        public static function getDefaultProject()
-        {
-            /** @var \Kreait\Laravel\Firebase\FirebaseProjectManager $instance */
-            return $instance->getDefaultProject();
-        }
-
-        /**
-         * @static
-         */
-        public static function setDefaultProject($name)
-        {
-            /** @var \Kreait\Laravel\Firebase\FirebaseProjectManager $instance */
-            return $instance->setDefaultProject($name);
-        }
-
-            }
-    }
-
 namespace Illuminate\Http {
     /**
      */
@@ -27721,7 +27678,6 @@ namespace  {
     class Validator extends \Illuminate\Support\Facades\Validator {}
     class View extends \Illuminate\Support\Facades\View {}
     class Vite extends \Illuminate\Support\Facades\Vite {}
-    class Firebase extends \Kreait\Laravel\Firebase\Facades\Firebase {}
 }
 
 

+ 1 - 0
app/Data/Pagarme/Request/CardRequestData/CardRequestData.php

@@ -9,6 +9,7 @@ final readonly class CardRequestData extends PagarmeData
     public function __construct(
         public string  $token,
         public ?string $label = null,
+
         public ?CardBillingAddressData $billingAddress = null,
     ) {
         self::requireFilled($this->token, 'token');

+ 1 - 0
app/Data/Pagarme/Request/CustomerRequestData/CustomerRequestData.php

@@ -14,6 +14,7 @@ final readonly class CustomerRequestData extends PagarmeData
         public string $type,
         public string $documentType,
         public string $code,
+
         public ?CustomerAddressRequestData $address = null,
         public ?CustomerPhonesRequestData  $phones  = null,
     ) {

+ 6 - 1
app/Data/Pagarme/Request/OrderRequestData/OrderPaymentData/OrderPaymentData.php

@@ -3,6 +3,8 @@
 namespace App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData;
 
 use App\Data\Pagarme\PagarmeData;
+use App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData\OrderPixData\OrderPixData;
+use App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData\OrderSplitData\OrderSplitData;
 
 final readonly class OrderPaymentData extends PagarmeData
 {
@@ -12,8 +14,11 @@ final readonly class OrderPaymentData extends PagarmeData
 
     public function __construct(
         public string $paymentMethod,
-        public ?OrderCreditCardData $creditCard = null, public ?OrderPixData $pix = null,
+
         public ?array $split = null,
+
+        public ?OrderCreditCardData $creditCard = null,
+        public ?OrderPixData        $pix        = null,
     ) {
         self::requireIn($this->paymentMethod, ['credit_card', 'pix'], 'payments.payment_method');
 

+ 1 - 1
app/Data/Pagarme/Request/OrderRequestData/OrderPaymentData/OrderPixAdditionalInformationData.php → app/Data/Pagarme/Request/OrderRequestData/OrderPaymentData/OrderPixData/OrderPixAdditionalInformationData.php

@@ -1,6 +1,6 @@
 <?php
 
-namespace App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData;
+namespace App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData\OrderPixData;
 
 use App\Data\Pagarme\PagarmeData;
 

+ 1 - 1
app/Data/Pagarme/Request/OrderRequestData/OrderPaymentData/OrderPixData.php → app/Data/Pagarme/Request/OrderRequestData/OrderPaymentData/OrderPixData/OrderPixData.php

@@ -1,6 +1,6 @@
 <?php
 
-namespace App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData;
+namespace App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData\OrderPixData;
 
 use App\Data\Pagarme\PagarmeData;
 

+ 2 - 1
app/Data/Pagarme/Request/OrderRequestData/OrderPaymentData/OrderSplitData.php → app/Data/Pagarme/Request/OrderRequestData/OrderPaymentData/OrderSplitData/OrderSplitData.php

@@ -1,6 +1,6 @@
 <?php
 
-namespace App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData;
+namespace App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData\OrderSplitData;
 
 use App\Data\Pagarme\PagarmeData;
 
@@ -10,6 +10,7 @@ final readonly class OrderSplitData extends PagarmeData
         public int    $amount,
         public string $recipientId,
         public string $type = 'flat',
+
         public ?OrderSplitOptionsData $options = null,
     ) {
         self::requirePositiveInt($this->amount, 'split.amount');

+ 1 - 1
app/Data/Pagarme/Request/OrderRequestData/OrderPaymentData/OrderSplitOptionsData.php → app/Data/Pagarme/Request/OrderRequestData/OrderPaymentData/OrderSplitData/OrderSplitOptionsData.php

@@ -1,6 +1,6 @@
 <?php
 
-namespace App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData;
+namespace App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData\OrderSplitData;
 
 use App\Data\Pagarme\PagarmeData;
 

+ 4 - 3
app/Data/Pagarme/Request/OrderRequestData/OrderRequestData.php

@@ -6,9 +6,9 @@ use App\Data\Pagarme\PagarmeData;
 use App\Data\Pagarme\Request\CustomerRequestData\CustomerRequestData as CustomerData;
 use App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData\OrderCreditCardData;
 use App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData\OrderPaymentData;
-use App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData\OrderPixData;
-use App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData\OrderSplitData;
-use App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData\OrderSplitOptionsData;
+use App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData\OrderPixData\OrderPixData;
+use App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData\OrderSplitData\OrderSplitData;
+use App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData\OrderSplitData\OrderSplitOptionsData;
 use App\Models\PaymentSplit;
 use Illuminate\Support\Collection;
 
@@ -18,6 +18,7 @@ final readonly class OrderRequestData extends PagarmeData
      * @param  OrderItemData[]  $items
      * @param  OrderPaymentData[]  $payments
      */
+
     public function __construct(
         public string  $code,
         public array   $items,

+ 3 - 1
app/Data/Pagarme/Request/RecipientRequestData/RecipientRegisterInformationData/RecipientRegisterInformationData.php

@@ -15,7 +15,9 @@ final readonly class RecipientRegisterInformationData extends PagarmeData
         public ?string $birthdate,
         public int     $monthlyIncome,
         public string  $professionalOccupation,
-        public RecipientPhoneNumbersData $phoneNumbers, public RecipientAddressData $address,
+
+        public RecipientPhoneNumbersData $phoneNumbers,
+        public RecipientAddressData      $address,
     ) {
         self::requireFilled($this->name, 'register_information.name');
         self::requireFilled($this->email, 'register_information.email');

+ 5 - 3
app/Data/Pagarme/Request/RecipientRequestData/RecipientRequestData.php

@@ -3,14 +3,16 @@
 namespace App\Data\Pagarme\Request\RecipientRequestData;
 
 use App\Data\Pagarme\PagarmeData;
+use App\Data\Pagarme\Request\RecipientRequestData\RecipientRegisterInformationData\RecipientRegisterInformationData;
 
 final readonly class RecipientRequestData extends PagarmeData
 {
     public function __construct(
         public string $code,
-        public RecipientRegisterInformationData $registerInformation,
-        public RecipientBankAccountData         $defaultBankAccount,
-        public RecipientTransferSettingsData    $transferSettings,
+
+        public RecipientRegisterInformationData           $registerInformation,
+        public RecipientBankAccountData                   $defaultBankAccount,
+        public RecipientTransferSettingsData              $transferSettings,
         public RecipientAutomaticAnticipationSettingsData $automaticAnticipationSettings,
     ) {
         self::requireFilled($this->code, 'code');

+ 2 - 0
app/Data/Pagarme/Response/CardResponseData.php

@@ -2,6 +2,8 @@
 
 namespace App\Data\Pagarme\Response;
 
+use App\Data\Pagarme\PagarmeResponseData;
+
 final readonly class CardResponseData extends PagarmeResponseData
 {
     public function __construct(

+ 4 - 0
app/Data/Pagarme/Response/OrderResponseData/OrderChargeResponseData/OrderChargeResponseData.php

@@ -13,6 +13,8 @@ final readonly class OrderChargeResponseData extends PagarmeResponseData
         public ?string $currency,
         public ?string $paidAt,
         public ?string $createdAt,
+        public ?string $expiresAt = null,
+
         public ?OrderTransactionResponseData $lastTransaction,
     ) {}
 
@@ -32,6 +34,7 @@ final readonly class OrderChargeResponseData extends PagarmeResponseData
             currency:  static::arrString($payload, 'currency'),
             paidAt:    static::arrString($payload, 'paid_at'),
             createdAt: static::arrString($payload, 'created_at'),
+            expiresAt: static::arrString($payload, 'expires_at'),
 
             lastTransaction: ! empty($transaction)
                 ? OrderTransactionResponseData::fromArray($transaction)
@@ -48,6 +51,7 @@ final readonly class OrderChargeResponseData extends PagarmeResponseData
             'currency'         => $this->currency,
             'paid_at'          => $this->paidAt,
             'created_at'       => $this->createdAt,
+            'expires_at'       => $this->expiresAt,
             'last_transaction' => $this->lastTransaction?->toArray(),
         ], static fn ($v) => $v !== null);
     }

+ 14 - 2
app/Data/Pagarme/Response/OrderResponseData/OrderChargeResponseData/OrderTransactionResponseData.php

@@ -10,9 +10,13 @@ final readonly class OrderTransactionResponseData extends PagarmeResponseData
         public ?string $id,
         public ?string $status,
         public ?int    $amount,
+        public ?int    $cost,
         public ?string $createdAt,
         public ?string $acquirerMessage,
         public array   $gatewayResponse,
+        public ?string $qrCode        = null,
+        public ?string $qrCodeUrl     = null,
+        public ?string $expiresAt     = null,
     ) {}
 
     public static function fromArray(array $payload): static
@@ -21,21 +25,29 @@ final readonly class OrderTransactionResponseData extends PagarmeResponseData
             id:              static::arrString($payload, 'id'),
             status:          static::arrString($payload, 'status'),
             amount:          static::arrInt($payload, 'amount'),
+            cost:            static::arrInt($payload, 'cost'),
             createdAt:       static::arrString($payload, 'created_at'),
             acquirerMessage: static::arrString($payload, 'acquirer_message'),
             gatewayResponse: static::arrArray($payload, 'gateway_response'),
+            qrCode:          static::arrString($payload, 'qr_code'),
+            qrCodeUrl:       static::arrString($payload, 'qr_code_url'),
+            expiresAt:       static::arrString($payload, 'expires_at'),
         );
     }
 
     public function toArray(): array
     {
-        return [
+        return array_filter([
             'id'               => $this->id,
             'status'           => $this->status,
             'amount'           => $this->amount,
+            'cost'             => $this->cost,
             'created_at'       => $this->createdAt,
             'acquirer_message' => $this->acquirerMessage,
             'gateway_response' => $this->gatewayResponse,
-        ];
+            'qr_code'          => $this->qrCode,
+            'qr_code_url'      => $this->qrCodeUrl,
+            'expires_at'       => $this->expiresAt,
+        ], static fn ($v) => $v !== null);
     }
 }

+ 60 - 3
app/Data/Pagarme/Response/OrderResponseData/OrderResponseData.php

@@ -57,7 +57,13 @@ final readonly class OrderResponseData extends PagarmeResponseData
             return null;
         }
 
-        return $this->filledValue($transaction->gatewayResponse['code'] ?? null);
+        $code = $this->filledValue($transaction->gatewayResponse['code'] ?? null);
+
+        if ($code === null || $this->isMisleadingFailureCode($code)) {
+            return $this->filledValue($transaction->status);
+        }
+
+        return $code;
     }
 
     public function failureMessage(): ?string
@@ -70,14 +76,14 @@ final readonly class OrderResponseData extends PagarmeResponseData
 
         $acquirerMessage = $this->filledValue($transaction->acquirerMessage);
 
-        if ($acquirerMessage) {
+        if ($acquirerMessage && ! $this->isMisleadingAcquirerMessage($acquirerMessage)) {
             return $acquirerMessage;
         }
 
         $gatewayErrors = $transaction->gatewayResponse['errors'] ?? [];
 
         if (! is_array($gatewayErrors) || empty($gatewayErrors)) {
-            return null;
+            return $this->failureMessageFromStatus($transaction->status);
         }
 
         $message = collect($gatewayErrors)
@@ -241,6 +247,57 @@ final readonly class OrderResponseData extends PagarmeResponseData
 
     //
 
+    private function isMisleadingFailureCode(string $code): bool
+    {
+        // filtra para nao incluir codigos http que nao sao de erro
+
+        if (preg_match('/^[1-5]\d{2}$/', $code)) {
+            return true;
+        }
+
+        $lower = mb_strtolower($code);
+
+        $successCodes = ['00', '0', 'approved', 'success'];
+
+        return in_array($lower, $successCodes, true);
+    }
+
+    private function isMisleadingAcquirerMessage(string $message): bool
+    {
+        $lower = mb_strtolower($message);
+
+        $successPatterns = [
+            'aprovada',
+            'aprovado',
+            'autorizada',
+            'autorizado',
+            'authorized',
+            'sucesso',
+        ];
+
+        foreach ($successPatterns as $pattern) {
+            if (str_contains($lower, $pattern)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    //
+
+    private function failureMessageFromStatus(?string $status): ?string
+    {
+        return match (strtolower((string) $status)) {
+            'not_authorized' => 'Transação não autorizada pela operadora do cartão.',
+            'with_error'     => 'Erro ao processar a transação.',
+            'failed'         => 'Transação falhou.',
+            'underpaid'      => 'Valor pago inferior ao esperado.',
+            'chargedback'    => 'Transação sofreu chargeback.',
+            default          => null,
+        };
+    }
+
     private function filledValue(mixed $value): ?string
     {
         if ($value === null || $value === '' || $value === []) {

+ 2 - 0
app/Data/Pagarme/Response/TransferResponseData.php

@@ -2,6 +2,8 @@
 
 namespace App\Data\Pagarme\Response;
 
+use App\Data\Pagarme\PagarmeResponseData;
+
 final readonly class TransferResponseData extends PagarmeResponseData
 {
     public function __construct(

+ 11 - 0
app/Http/Controllers/PaymentController.php

@@ -72,6 +72,17 @@ class PaymentController extends Controller
         );
     }
 
+    public function getSchedulePix(Schedule $schedule): JsonResponse
+    {
+        if ($schedule->client?->user_id !== Auth::id()) {
+            abort(403);
+        }
+
+        $item = $this->service->getOrCreatePixPayment($schedule);
+
+        return $this->successResponse(payload: new PaymentResource($item));
+    }
+
     public function show(int $id): JsonResponse
     {
         $item = $this->service->findById($id);

+ 7 - 2
app/Http/Requests/AddressRequest.php

@@ -11,11 +11,13 @@ class AddressRequest extends FormRequest
         $rules = [
             'source'    => 'sometimes|string|in:provider,client',
             'source_id' => 'sometimes|integer|min:1',
-            'zip_code'  => [
+
+            'zip_code' => [
                 'sometimes',
                 'string',
                 'regex:/^[0-9]{8}$/',
             ],
+
             'address'        => 'sometimes|string',
             'number'         => 'nullable|string|max:20',
             'district'       => 'nullable|string|max:255',
@@ -32,17 +34,20 @@ class AddressRequest extends FormRequest
         ];
 
         if ($this->isMethod('post')) {
-            $rules['source']    = 'required|string|in:provider,client';
+            $rules['source'] = 'required|string|in:provider,client';
+
             $rules['source_id'] = [
                 'required',
                 'integer',
                 'min:1',
             ];
+
             $rules['zip_code'] = [
                 'required',
                 'string',
                 'regex:/^[0-9]{8}$/',
             ];
+
             $rules['address']      = 'required|string';
             $rules['address_type'] = 'required|in:home,commercial,other';
         }

+ 7 - 2
app/Http/Requests/CityRequest.php

@@ -13,10 +13,12 @@ class CityRequest extends FormRequest
         $rules = [
             'name'       => 'sometimes|string|max:255',
             'country_id' => 'sometimes|exists:countries,id',
-            'state_id'   => [
+
+            'state_id' => [
                 'sometimes',
                 'exists:states,id',
             ],
+
             'status' => ['sometimes', Rule::enum(DefaultStatusEnum::class)],
         ];
 
@@ -29,11 +31,14 @@ class CityRequest extends FormRequest
                     return $query->where('state_id', $this->state_id);
                 }),
             ];
+
             $rules['country_id'] = 'required|exists:countries,id';
-            $rules['state_id']   = [
+
+            $rules['state_id'] = [
                 'required',
                 'exists:states,id',
             ];
+
             $rules['status'] = ['required', Rule::enum(DefaultStatusEnum::class)];
         }
 

+ 5 - 2
app/Http/Requests/ClientFavoriteProviderRequest.php

@@ -18,7 +18,8 @@ class ClientFavoriteProviderRequest extends FormRequest
         $favoriteId = $this->route('id');
 
         $rules = [
-            'client_id'   => ['sometimes', 'integer', 'exists:clients,id'],
+            'client_id' => ['sometimes', 'integer', 'exists:clients,id'],
+
             'provider_id' => [
                 'sometimes',
                 'integer',
@@ -28,11 +29,13 @@ class ClientFavoriteProviderRequest extends FormRequest
                     ->whereNull('deleted_at')
                     ->ignore($favoriteId),
             ],
+
             'notes' => ['nullable', 'string', 'max:1000'],
         ];
 
         if ($this->isMethod('POST')) {
-            $rules['client_id']   = ['required', 'integer', 'exists:clients,id'];
+            $rules['client_id'] = ['required', 'integer', 'exists:clients,id'];
+
             $rules['provider_id'] = [
                 'required',
                 'integer',

+ 4 - 2
app/Http/Requests/ClientProviderBlockRequest.php

@@ -18,7 +18,8 @@ class ClientProviderBlockRequest extends FormRequest
         $blockId  = $this->route('id');
 
         $rules = [
-            'client_id'   => ['sometimes', 'integer', 'exists:clients,id'],
+            'client_id' => ['sometimes', 'integer', 'exists:clients,id'],
+
             'provider_id' => [
                 'sometimes',
                 'integer',
@@ -31,7 +32,8 @@ class ClientProviderBlockRequest extends FormRequest
         ];
 
         if ($this->isMethod('POST')) {
-            $rules['client_id']   = ['required', 'integer', 'exists:clients,id'];
+            $rules['client_id'] = ['required', 'integer', 'exists:clients,id'];
+
             $rules['provider_id'] = [
                 'required',
                 'integer',

+ 6 - 0
app/Http/Requests/ClientRequest.php

@@ -21,6 +21,7 @@ class ClientRequest extends FormRequest
                     }
                 },
             ],
+
             'user_id' => [
                 'sometimes',
                 'exists:users,id',
@@ -60,6 +61,7 @@ class ClientRequest extends FormRequest
                     }
                 },
             ];
+
             $rules['user_id'] = [
                 'required',
                 'exists:users,id',
@@ -111,7 +113,9 @@ class ClientRequest extends FormRequest
             for ($d = 0, $c = 0; $c < $t; $c++) {
                 $d += $cpf[$c] * (($t + 1) - $c);
             }
+
             $d = ((10 * $d) % 11) % 10;
+
             if ($cpf[$c] != $d) {
                 return false;
             }
@@ -134,6 +138,7 @@ class ClientRequest extends FormRequest
 
         for ($i = $length; $i >= 1; $i--) {
             $sum += $numbers[$length - $i] * $pos--;
+
             if ($pos < 2) {
                 $pos = 9;
             }
@@ -152,6 +157,7 @@ class ClientRequest extends FormRequest
 
         for ($i = $length; $i >= 1; $i--) {
             $sum += $numbers[$length - $i] * $pos--;
+
             if ($pos < 2) {
                 $pos = 9;
             }

+ 4 - 2
app/Http/Requests/ProviderClientBlockRequest.php

@@ -19,7 +19,8 @@ class ProviderClientBlockRequest extends FormRequest
 
         $rules = [
             'provider_id' => ['sometimes', 'integer', 'exists:providers,id'],
-            'client_id'   => [
+
+            'client_id' => [
                 'sometimes',
                 'integer',
                 'exists:clients,id',
@@ -32,7 +33,8 @@ class ProviderClientBlockRequest extends FormRequest
 
         if ($this->isMethod('POST')) {
             $rules['provider_id'] = ['required', 'integer', 'exists:providers,id'];
-            $rules['client_id']   = [
+
+            $rules['client_id'] = [
                 'required',
                 'integer',
                 'exists:clients,id',

+ 23 - 13
app/Http/Requests/ProviderRequest.php

@@ -2,10 +2,10 @@
 
 namespace App\Http\Requests;
 
+use App\Enums\ApprovalStatusEnum;
+use App\Enums\UserTypeEnum;
 use Illuminate\Foundation\Http\FormRequest;
 use Illuminate\Validation\Rule;
-use App\Enums\UserTypeEnum;
-use App\Enums\ApprovalStatusEnum;
 
 class ProviderRequest extends FormRequest
 {
@@ -27,7 +27,9 @@ class ProviderRequest extends FormRequest
                     ->ignore($providerId)
                     ->whereNull('deleted_at'),
             ],
+
             'rg' => 'sometimes|nullable|string|max:20',
+
             'user_id' => [
                 'sometimes',
                 'exists:users,id',
@@ -51,11 +53,13 @@ class ProviderRequest extends FormRequest
                             $query->where('id', '!=', $providerId);
                         })
                         ->exists();
+
                     if ($providerExists) {
                         $fail(__('validation.custom.user_id.already_linked_to_provider'));
                     }
                 },
             ],
+
             'average_rating'         => 'sometimes|nullable|numeric|min:0|max:5',
             'total_services'         => 'sometimes|integer|min:0',
             'birth_date'             => 'sometimes|nullable|date|before:today',
@@ -73,7 +77,6 @@ class ProviderRequest extends FormRequest
             'recipient_description'  => 'sometimes|nullable|string',
             'recipient_document'     => 'sometimes|string|max:20',
             'recipient_type'         => ['sometimes', Rule::in(['individual', 'company'])],
-            'recipient_code'         => 'sometimes|string|max:255',
             'recipient_payment_mode' => ['sometimes', Rule::in(['bank_transfer'])],
             'recipient_metadata'     => 'sometimes|array',
         ];
@@ -90,18 +93,18 @@ class ProviderRequest extends FormRequest
                 },
                 Rule::unique('providers', 'document')->whereNull('deleted_at'),
             ];
+
             $rules['recipient_name']                                     = 'required|string|max:255';
             $rules['recipient_email']                                    = 'required|email|max:255';
             $rules['recipient_description']                              = 'required|string';
             $rules['recipient_document']                                 = 'required|string|max:20';
             $rules['recipient_type']                                     = ['required', Rule::in(['individual', 'company'])];
-            $rules['recipient_code']                                     = 'required|string|max:255';
             $rules['recipient_payment_mode']                             = ['required', Rule::in(['bank_transfer'])];
             $rules['recipient_default_bank_account']                     = 'required|array';
             $rules['recipient_default_bank_account.holder_name']         = 'required|string|max:255';
             $rules['recipient_default_bank_account.holder_type']         = ['required', Rule::in(['individual', 'company'])];
             $rules['recipient_default_bank_account.holder_document']     = 'required|string|max:20';
-            $rules['recipient_default_bank_account.bank']                = 'required|string|max:20';
+            $rules['recipient_default_bank_account.bank']                = 'required|string|max:3';
             $rules['recipient_default_bank_account.branch_number']       = 'required|string|max:20';
             $rules['recipient_default_bank_account.branch_check_digit']  = 'sometimes|nullable|string|max:10';
             $rules['recipient_default_bank_account.account_number']      = 'required|string|max:20';
@@ -109,7 +112,8 @@ class ProviderRequest extends FormRequest
             $rules['recipient_default_bank_account.type']                = ['required', Rule::in(['checking', 'savings'])];
             $rules['recipient_default_bank_account.metadata']            = 'required|array';
             $rules['recipient_metadata']                                 = 'required|array';
-            $rules['user_id']                                            = [
+
+            $rules['user_id'] = [
                 'required',
                 'exists:users,id',
                 Rule::unique('providers', 'user_id')->whereNull('deleted_at'),
@@ -127,6 +131,7 @@ class ProviderRequest extends FormRequest
                         ->where('user_id', $value)
                         ->whereNull('deleted_at')
                         ->exists();
+
                     if ($providerExists) {
                         $fail(__('validation.custom.user_id.already_linked_to_provider'));
                     }
@@ -168,7 +173,9 @@ class ProviderRequest extends FormRequest
             for ($d = 0, $c = 0; $c < $t; $c++) {
                 $d += $cpf[$c] * (($t + 1) - $c);
             }
+
             $d = ((10 * $d) % 11) % 10;
+
             if ($cpf[$c] != $d) {
                 return false;
             }
@@ -188,14 +195,15 @@ class ProviderRequest extends FormRequest
         }
 
         // Valida primeiro dígito verificador
-        $length = strlen($cnpj) - 2;
+        $length  = strlen($cnpj) - 2;
         $numbers = substr($cnpj, 0, $length);
-        $digits = substr($cnpj, $length);
-        $sum = 0;
-        $pos = $length - 7;
+        $digits  = substr($cnpj, $length);
+        $sum     = 0;
+        $pos     = $length - 7;
 
         for ($i = $length; $i >= 1; $i--) {
             $sum += $numbers[$length - $i] * $pos--;
+
             if ($pos < 2) {
                 $pos = 9;
             }
@@ -208,13 +216,15 @@ class ProviderRequest extends FormRequest
         }
 
         // Valida segundo dígito verificador
-        $length = $length + 1;
+
+        $length  = $length + 1;
         $numbers = substr($cnpj, 0, $length);
-        $sum = 0;
-        $pos = $length - 7;
+        $sum     = 0;
+        $pos     = $length - 7;
 
         for ($i = $length; $i >= 1; $i--) {
             $sum += $numbers[$length - $i] * $pos--;
+
             if ($pos < 2) {
                 $pos = 9;
             }

+ 12 - 10
app/Http/Requests/RegisterProviderRequest.php

@@ -11,12 +11,12 @@ class RegisterProviderRequest extends FormRequest
   public function rules(): array
   {
     $rules = [
-      'email' => 'sometimes|email',
-      'phone' => 'sometimes|string|nullable|max:20',
-      'name' => 'required|string|max:255',
-      'code' => 'required|string|max:6',
-      'document' => ['required', 'string', 'max:20'],
-      'rg' => 'required|string|max:20',
+      'email'      => 'sometimes|email',
+      'phone'      => 'sometimes|string|nullable|max:20',
+      'name'       => 'required|string|max:255',
+      'code'       => 'required|string|max:6',
+      'document'   => ['required', 'string', 'max:20'],
+      'rg'         => 'required|string|max:20',
       'birth_date' => 'required|date|before:today',
 
       'recipient_name'        => 'sometimes|string|max:255',
@@ -24,7 +24,6 @@ class RegisterProviderRequest extends FormRequest
       'recipient_description' => 'sometimes|string',
       'recipient_document'    => 'sometimes|string|max:20',
       'recipient_type'        => ['sometimes', Rule::in(['individual', 'company'])],
-      'recipient_code'        => 'sometimes|string|max:255',
 
       'recipient_payment_mode' => ['sometimes', Rule::in(['bank_transfer'])],
 
@@ -32,7 +31,7 @@ class RegisterProviderRequest extends FormRequest
       'recipient_default_bank_account.holder_name'         => 'sometimes|string|max:255',
       'recipient_default_bank_account.holder_type'         => ['sometimes', Rule::in(['individual', 'company'])],
       'recipient_default_bank_account.holder_document'     => 'sometimes|string|max:20',
-      'recipient_default_bank_account.bank'                => 'sometimes|string|max:20',
+      'recipient_default_bank_account.bank'                => 'sometimes|string|max:3',
       'recipient_default_bank_account.branch_number'       => 'sometimes|string|max:20',
       'recipient_default_bank_account.branch_check_digit'  => 'sometimes|nullable|string|max:10',
       'recipient_default_bank_account.account_number'      => 'sometimes|string|max:20',
@@ -59,13 +58,16 @@ class RegisterProviderRequest extends FormRequest
       'daily_price_2h' => 'required|numeric|min:0',
 
       'services_types_ids' => 'sometimes|array',
+
       'services_types_ids.*' => [
         'integer',
         Rule::exists('service_types', 'id')->where(function ($query) {
           $query->whereNull('deleted_at')->where('is_active', true);
         }),
       ],
+
       'service_types_ids' => 'sometimes|array',
+
       'service_types_ids.*' => [
         'integer',
         Rule::exists('service_types', 'id')->where(function ($query) {
@@ -73,8 +75,8 @@ class RegisterProviderRequest extends FormRequest
         }),
       ],
 
-      'working_days' => 'required|array|min:1',
-      'working_days.*.day' => 'required|integer|min:0|max:6',
+      'working_days'          => 'required|array|min:1',
+      'working_days.*.day'    => 'required|integer|min:0|max:6',
       'working_days.*.period' => ['required', Rule::in([WorkingPeriodEnum::MORNING->value, WorkingPeriodEnum::AFTERNOON->value])],
 
       'selfie'          => 'required|file|image|mimes:jpg,jpeg,png,webp|max:5120',

+ 2 - 1
app/Http/Requests/ReviewImprovementRequest.php

@@ -23,7 +23,8 @@ class ReviewImprovementRequest extends FormRequest
         ];
 
         if ($this->isMethod('POST')) {
-            $rules['review_id']           = ['required', 'integer', 'exists:reviews,id'];
+            $rules['review_id'] = ['required', 'integer', 'exists:reviews,id'];
+
             $rules['improvement_type_id'] = [
                 'required',
                 'integer',

+ 3 - 1
app/Http/Requests/ReviewRequest.php

@@ -38,7 +38,8 @@ class ReviewRequest extends FormRequest
 
             $rules['schedule_id'] = ['required', 'integer', 'exists:schedules,id'];
             $rules['origin']      = ['required', 'string', Rule::in(['provider', 'client'])];
-            $rules['origin_id']   = [
+
+            $rules['origin_id'] = [
                 'required',
                 'integer',
                 Rule::unique('reviews', 'origin_id')
@@ -47,6 +48,7 @@ class ReviewRequest extends FormRequest
                     ->whereNull('deleted_at')
                     ->ignore($reviewId),
             ];
+
             $rules['stars']              = ['required', 'numeric', 'min:0', 'max:5'];
             $rules['improvements_ids']   = ['sometimes', 'array'];
             $rules['improvements_ids.*'] = ['integer', 'exists:improvement_types,id'];

+ 2 - 0
app/Http/Requests/StateRequest.php

@@ -26,6 +26,7 @@ class StateRequest extends FormRequest
                     return $query->where('country_id', $this->country_id);
                 }),
             ];
+
             $rules['code'] = [
                 'required',
                 'string',
@@ -34,6 +35,7 @@ class StateRequest extends FormRequest
                     return $query->where('country_id', $this->country_id);
                 }),
             ];
+
             $rules['country_id'] = 'required|exists:countries,id';
             $rules['status']     = ['required', Rule::enum(DefaultStatusEnum::class)];
         }

+ 1 - 0
app/Http/Requests/UserAppsValidateCodeRequest.php

@@ -15,6 +15,7 @@ class UserAppsValidateCodeRequest extends FormRequest
         if ($this->has('email')) {
             $rules['email'] = 'required|email';
         }
+
         if (! $this->has('phone')) {
             $rules['phone'] = 'required|string|max:20';
         }

+ 4 - 1
app/Http/Requests/UserRequest.php

@@ -14,11 +14,13 @@ class UserRequest extends FormRequest
         $rules = [
             'avatar' => 'sometimes|string|nullable',
             'name'   => 'sometimes|string|nullable',
-            'email'  => [
+
+            'email' => [
                 'sometimes',
                 'email',
                 Rule::unique('users', 'email')->ignore($this->route('id')),
             ],
+
             'password' => 'sometimes|string|nullable',
             'type'     => ['sometimes', Rule::enum(UserTypeEnum::class)],
             'language' => ['sometimes', Rule::enum(LanguageEnum::class)],
@@ -29,6 +31,7 @@ class UserRequest extends FormRequest
             $rules['name']     = 'required|string|max:255';
             $rules['email']    = 'required|email|unique:users,email';
             $rules['password'] = 'required|string|min:6';
+
             if (! $this->has('language')) {
                 $this->merge(['language' => LanguageEnum::PORTUGUESE->value]);
             }

+ 7 - 2
app/Http/Resources/PaymentResource.php

@@ -67,12 +67,17 @@ class PaymentResource extends JsonResource
             return null;
         }
 
-        $transaction = $this->gateway_payload['charges'][0]['last_transaction'] ?? [];
+        $charge = $this->gateway_payload['charges'][0] ?? [];
+        $transaction = $charge['last_transaction'] ?? [];
+
+        $expiresAt = $charge['expires_at']
+            ?? $transaction['expires_at']
+            ?? $this->expires_at?->toISOString();
 
         return [
             'qr_code'     => $transaction['qr_code'] ?? null,
             'qr_code_url' => $transaction['qr_code_url'] ?? null,
-            'expires_at'  => $transaction['expires_at'] ?? $this->expires_at?->toISOString(),
+            'expires_at'  => $expiresAt,
             'status'      => $transaction['status'] ?? null,
         ];
     }

+ 25 - 6
app/Models/Client.php

@@ -13,19 +13,20 @@ use Illuminate\Database\Eloquent\SoftDeletes;
  * @property int $id
  * @property string|null $document
  * @property int $user_id
- * @property int|null $profile_media_id
- * @property-read \App\Models\Media|null $profileMedia
  * @property \Illuminate\Support\Carbon|null $created_at
  * @property \Illuminate\Support\Carbon|null $updated_at
  * @property \Illuminate\Support\Carbon|null $deleted_at
  * @property string|null $average_rating
  * @property int $total_services
- * @property string|null $external_customer_id
- * @property string|null $external_customer_code
+ * @property string|null $gateway_customer_id
+ * @property string|null $gateway_customer_code
+ * @property int|null $profile_media_id
+ * @property string|null $idempotency_key
  * @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProviderClientBlock> $blockedByProviders
  * @property-read int|null $blocked_by_providers_count
  * @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ClientProviderBlock> $blockedProviders
  * @property-read int|null $blocked_providers_count
+ * @property-read \App\Models\Media|null $profileMedia
  * @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Schedule> $schedules
  * @property-read int|null $schedules_count
  * @property-read \App\Models\User $user
@@ -40,6 +41,8 @@ use Illuminate\Database\Eloquent\SoftDeletes;
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Client whereExternalCustomerCode($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Client whereExternalCustomerId($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Client whereId($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|Client whereIdempotencyKey($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|Client whereProfileMediaId($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Client whereTotalServices($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Client whereUpdatedAt($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Client whereUserId($value)
@@ -53,8 +56,9 @@ class Client extends Model
 
     protected $fillable = [
         'document',
-        'external_customer_id',
-        'external_customer_code',
+        'gateway_customer_id',
+        'gateway_customer_code',
+        'idempotency_key',
         'user_id',
         'profile_media_id',
     ];
@@ -107,4 +111,19 @@ class Client extends Model
     {
         return $this->hasMany(Schedule::class);
     }
+
+    //
+
+    public function ensureGatewayCode(): string
+    {
+        if (! empty($this->gateway_customer_code)) {
+            return $this->gateway_customer_code;
+        }
+
+        $code = 'client-'.(string) \Illuminate\Support\Str::uuid();
+
+        $this->forceFill(['gateway_customer_code' => $code])->save();
+
+        return $code;
+    }
 }

+ 3 - 0
app/Models/ClientPaymentMethod.php

@@ -23,6 +23,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
  * @property \Illuminate\Support\Carbon|null $deleted_at
  * @property string|null $token
  * @property string|null $gateway_card_id
+ * @property string|null $idempotency_key
  * @property-read \App\Models\Client $client
  * @method static \Illuminate\Database\Eloquent\Builder<static>|ClientPaymentMethod newModelQuery()
  * @method static \Illuminate\Database\Eloquent\Builder<static>|ClientPaymentMethod newQuery()
@@ -39,6 +40,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
  * @method static \Illuminate\Database\Eloquent\Builder<static>|ClientPaymentMethod whereGatewayCardId($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|ClientPaymentMethod whereHolderName($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|ClientPaymentMethod whereId($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ClientPaymentMethod whereIdempotencyKey($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|ClientPaymentMethod whereIsActive($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|ClientPaymentMethod whereLastFourDigits($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|ClientPaymentMethod whereToken($value)
@@ -55,6 +57,7 @@ class ClientPaymentMethod extends Model
         'client_id',
         'token',
         'gateway_card_id',
+        'idempotency_key',
         'card_number',
         'holder_name',
         'expiration',

+ 9 - 2
app/Models/DeviceToken.php

@@ -16,10 +16,17 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
  * @property \Illuminate\Support\Carbon|null $created_at
  * @property \Illuminate\Support\Carbon|null $updated_at
  * @property-read \App\Models\User $user
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|DeviceToken newModelQuery()
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|DeviceToken newQuery()
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|DeviceToken query()
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|DeviceToken whereActive($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|DeviceToken whereAppType($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|DeviceToken whereUserId($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|DeviceToken whereCreatedAt($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|DeviceToken whereId($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|DeviceToken wherePlatform($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|DeviceToken whereToken($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|DeviceToken whereActive($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|DeviceToken whereUpdatedAt($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|DeviceToken whereUserId($value)
  * @mixin \Eloquent
  */
 class DeviceToken extends Model

+ 34 - 0
app/Models/Notification.php

@@ -6,6 +6,40 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\SoftDeletes;
 
+/**
+ * @property int $id
+ * @property string $title
+ * @property string $description
+ * @property string $origin
+ * @property int $origin_id
+ * @property string $type
+ * @property bool $read
+ * @property \Illuminate\Support\Carbon|null $read_at
+ * @property int|null $user_id
+ * @property \Illuminate\Support\Carbon|null $deleted_at
+ * @property \Illuminate\Support\Carbon|null $created_at
+ * @property \Illuminate\Support\Carbon|null $updated_at
+ * @property-read \App\Models\User|null $user
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|Notification newModelQuery()
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|Notification newQuery()
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|Notification onlyTrashed()
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|Notification query()
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|Notification whereCreatedAt($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|Notification whereDeletedAt($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|Notification whereDescription($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|Notification whereId($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|Notification whereOrigin($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|Notification whereOriginId($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|Notification whereRead($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|Notification whereReadAt($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|Notification whereTitle($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|Notification whereType($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|Notification whereUpdatedAt($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|Notification whereUserId($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|Notification withTrashed(bool $withTrashed = true)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|Notification withoutTrashed()
+ * @mixin \Eloquent
+ */
 class Notification extends Model
 {
     use HasFactory, SoftDeletes;

+ 18 - 0
app/Models/Payment.php

@@ -38,6 +38,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
  * @property \Illuminate\Support\Carbon|null $updated_at
  * @property \Illuminate\Support\Carbon|null $deleted_at
  * @property string|null $gateway_code
+ * @property string|null $idempotency_key
  * @property-read \App\Models\Client $client
  * @property-read \App\Models\ClientPaymentMethod|null $clientPaymentMethod
  * @property-read \App\Models\Provider|null $provider
@@ -71,6 +72,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Payment whereGatewayProvider($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Payment whereGrossAmount($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Payment whereId($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|Payment whereIdempotencyKey($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Payment whereInstallments($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Payment whereMetadata($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Payment whereNetAmount($value)
@@ -98,6 +100,7 @@ class Payment extends Model
         'client_payment_method_id',
         'gateway_provider',
         'gateway_code',
+        'idempotency_key',
         'gateway_entity_reference',
         'gateway_entity_label',
         'gateway_operation_reference',
@@ -169,4 +172,19 @@ class Payment extends Model
     {
         return $this->hasMany(Webhook::class);
     }
+
+    //
+
+    public function ensureGatewayCode(): string
+    {
+        if (! empty($this->gateway_code)) {
+            return $this->gateway_code;
+        }
+
+        $code = 'payment-'.(string) \Illuminate\Support\Str::uuid();
+
+        $this->forceFill(['gateway_code' => $code])->save();
+
+        return $code;
+    }
 }

+ 29 - 12
app/Models/Provider.php

@@ -15,21 +15,16 @@ use Illuminate\Database\Eloquent\SoftDeletes;
  * @property string $document
  * @property string|null $rg
  * @property int $user_id
- * @property float|null $average_rating
+ * @property numeric|null $average_rating
  * @property int $total_services
- * @property string|null $birth_date
+ * @property \Illuminate\Support\Carbon|null $birth_date
  * @property bool $selfie_verified
  * @property bool $document_verified
- * @property string $approval_status
- * @property float|null $daily_price_8h
- * @property float|null $daily_price_6h
- * @property float|null $daily_price_4h
- * @property float|null $daily_price_2h
+ * @property numeric|null $daily_price_8h
+ * @property numeric|null $daily_price_6h
+ * @property numeric|null $daily_price_4h
+ * @property numeric|null $daily_price_2h
  * @property int|null $profile_media_id
- * @property int|null $document_front_media_id
- * @property int|null $document_back_media_id
- * @property-read \App\Models\Media|null $documentFrontMedia
- * @property-read \App\Models\Media|null $documentBackMedia
  * @property \Illuminate\Support\Carbon|null $created_at
  * @property \Illuminate\Support\Carbon|null $updated_at
  * @property \Illuminate\Support\Carbon|null $deleted_at
@@ -46,14 +41,20 @@ use Illuminate\Database\Eloquent\SoftDeletes;
  * @property array<array-key, mixed>|null $recipient_transfer_settings
  * @property array<array-key, mixed>|null $recipient_automatic_anticipation_settings
  * @property array<array-key, mixed>|null $recipient_metadata
+ * @property int|null $document_front_media_id
+ * @property int|null $document_back_media_id
+ * @property string|null $idempotency_key
  * @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Address> $addresses
  * @property-read int|null $addresses_count
  * @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ClientProviderBlock> $blockedByClients
  * @property-read int|null $blocked_by_clients_count
  * @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProviderClientBlock> $blockedClients
  * @property-read int|null $blocked_clients_count
+ * @property-read \App\Models\Media|null $documentBackMedia
+ * @property-read \App\Models\Media|null $documentFrontMedia
  * @property-read \App\Models\Address|null $primaryAddress
  * @property-read \App\Models\Media|null $profileMedia
+ * @property-read \App\Models\User $user
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Provider newModelQuery()
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Provider newQuery()
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Provider onlyTrashed()
@@ -72,6 +73,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Provider whereDocumentFrontMediaId($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Provider whereDocumentVerified($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Provider whereId($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|Provider whereIdempotencyKey($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Provider whereProfileMediaId($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Provider whereRecipientAutomaticAnticipationSettings($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Provider whereRecipientCode($value)
@@ -100,7 +102,7 @@ class Provider extends Model
 
   protected $table = "providers";
 
-  protected $guarded = ["id"];
+  protected $guarded = ["id", "recipient_code"];
 
     /**
      * Get the attributes that should be cast.
@@ -209,4 +211,19 @@ class Provider extends Model
       ->where("source", "provider")
       ->orderBy("is_primary", "desc");
   }
+
+  //
+
+  public function ensureGatewayCode(): string
+  {
+      if (! empty($this->recipient_code)) {
+          return $this->recipient_code;
+      }
+
+      $code = 'provider-'.(string) \Illuminate\Support\Str::uuid();
+
+      $this->forceFill(['recipient_code' => $code])->save();
+
+      return $code;
+  }
 }

+ 9 - 2
app/Models/PushNotificationLog.php

@@ -16,10 +16,17 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
  * @property \Illuminate\Support\Carbon|null $created_at
  * @property \Illuminate\Support\Carbon|null $updated_at
  * @property-read \App\Models\User $user
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|PushNotificationLog newModelQuery()
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|PushNotificationLog newQuery()
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|PushNotificationLog query()
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|PushNotificationLog whereCategory($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|PushNotificationLog whereCreatedAt($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|PushNotificationLog whereId($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|PushNotificationLog whereLabel($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|PushNotificationLog whereUserId($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|PushNotificationLog whereSentAt($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|PushNotificationLog whereTarget($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|PushNotificationLog whereCategory($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|PushNotificationLog whereUpdatedAt($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|PushNotificationLog whereUserId($value)
  * @mixin \Eloquent
  */
 class PushNotificationLog extends Model

+ 2 - 2
app/Models/Review.php

@@ -19,12 +19,12 @@ use Illuminate\Database\Eloquent\SoftDeletes;
  * @property \Illuminate\Support\Carbon|null $created_at
  * @property \Illuminate\Support\Carbon|null $updated_at
  * @property \Illuminate\Support\Carbon|null $deleted_at
- * @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ReviewMedia> $reviewMedia
- * @property-read int|null $review_media_count
  * @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ImprovementType> $improvements
  * @property-read int|null $improvements_count
  * @property-read \App\Models\Client|null $originClient
  * @property-read \App\Models\Provider|null $originProvider
+ * @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ReviewMedia> $reviewMedia
+ * @property-read int|null $review_media_count
  * @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ReviewImprovement> $reviewsImprovements
  * @property-read int|null $reviews_improvements_count
  * @property-read \App\Models\Schedule $schedule

+ 12 - 0
app/Models/ReviewMedia.php

@@ -10,8 +10,20 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
  * @property int $review_id
  * @property int $media_id
  * @property string $origin
+ * @property \Illuminate\Support\Carbon|null $created_at
+ * @property \Illuminate\Support\Carbon|null $updated_at
  * @property-read \App\Models\Media $media
  * @property-read \App\Models\Review $review
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ReviewMedia newModelQuery()
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ReviewMedia newQuery()
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ReviewMedia query()
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ReviewMedia whereCreatedAt($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ReviewMedia whereId($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ReviewMedia whereMediaId($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ReviewMedia whereOrigin($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ReviewMedia whereReviewId($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ReviewMedia whereUpdatedAt($value)
+ * @mixin \Eloquent
  */
 class ReviewMedia extends Model
 {

+ 18 - 2
app/Models/Schedule.php

@@ -99,8 +99,8 @@ class Schedule extends Model
             'user_id',
             'document',
             'average_rating',
-            'external_customer_id',
-            'external_customer_code',
+            'gateway_customer_id',
+            'gateway_customer_code',
         );
     }
 
@@ -133,4 +133,20 @@ class Schedule extends Model
     {
         return $this->hasMany(Review::class);
     }
+
+    //
+
+    public function ensureCustomerPhone(?string $fallbackPhone = null): void
+    {
+        $phone  = $this->client?->user?->phone ?? $fallbackPhone;
+        $digits = preg_replace('/\D+/', '', (string) $phone) ?? '';
+
+        if (strlen($digits) >= 10) {
+            return;
+        }
+
+        throw new \InvalidArgumentException(
+            'Voce precisa cadastrar um numero de celular valido no seu perfil para concluir o pagamento.'
+        );
+    }
 }

+ 4 - 0
app/Models/User.php

@@ -27,11 +27,15 @@ use Laravel\Sanctum\HasApiTokens;
  * @property bool $validated_code
  * @property bool $registration_complete
  * @property-read \App\Models\Client|null $client
+ * @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\DeviceToken> $deviceTokens
+ * @property-read int|null $device_tokens_count
  * @property-read \Illuminate\Notifications\DatabaseNotificationCollection<int, \Illuminate\Notifications\DatabaseNotification> $notifications
  * @property-read int|null $notifications_count
  * @property-read \Kalnoy\Nestedset\Collection<int, \App\Models\Permission> $permissions
  * @property-read int|null $permissions_count
  * @property-read \App\Models\Provider|null $provider
+ * @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\PushNotificationLog> $pushNotificationLogs
+ * @property-read int|null $push_notification_logs_count
  * @property-read \Illuminate\Database\Eloquent\Collection<int, \Laravel\Sanctum\PersonalAccessToken> $tokens
  * @property-read int|null $tokens_count
  * @method static \Database\Factories\UserFactory factory($count = null, $state = [])

+ 91 - 0
app/Services/Pagarme/Concerns/FormatsPagarmeData.php

@@ -0,0 +1,91 @@
+<?php
+
+namespace App\Services\Pagarme\Concerns;
+
+use Carbon\Carbon;
+use Illuminate\Support\Str;
+
+trait FormatsPagarmeData
+{
+    protected function digits(?string $value): string
+    {
+        return preg_replace('/\D+/', '', (string) $value) ?? '';
+    }
+
+    protected 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'          => $streetSegment,
+            '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'),
+        ];
+    }
+
+    protected 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');
+    }
+
+    protected 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, '');
+    }
+}

+ 32 - 9
app/Services/Pagarme/Concerns/SendsPagarmeRequests.php

@@ -2,6 +2,7 @@
 
 namespace App\Services\Pagarme\Concerns;
 
+use App\Data\Pagarme\PagarmeData;
 use Illuminate\Support\Facades\Http;
 use Illuminate\Support\Facades\Log;
 use Throwable;
@@ -9,14 +10,13 @@ use Throwable;
 trait SendsPagarmeRequests
 {
     protected function pagarmeRequest(
-        string $method,
-        string $path,
-        string $idempotencyKey,
-        string $errorMessage,
-        array|Data $payload,
+        string            $method,
+        string            $path,
+        string            $idempotencyKey,
+        string            $errorMessage,
+        array|PagarmeData $payload,
     ): array {
-        $payload = $payload instanceof Data ? $payload->toArray() : $payload;
-
+        $payload  = $payload instanceof PagarmeData ? $payload->toArray() : $payload;
         $endpoint = $this->pagarmeUrl($path);
 
         try {
@@ -38,7 +38,13 @@ trait SendsPagarmeRequests
 
             return $result;
         } catch (Throwable $e) {
-            $responseBody = method_exists($e, 'getResponse') ? $e->getResponse()?->json() : null;
+            $responseBody = null;
+
+            if (method_exists($e, 'getResponse')) {
+                $responseBody = $e->getResponse()?->json();
+            } elseif (isset($e->response)) {
+                $responseBody = $e->response->json();
+            }
 
             Log::channel('pagarme')->error('Pagar.me request failed', [
                 'method'    => strtoupper($method),
@@ -49,7 +55,24 @@ trait SendsPagarmeRequests
                 'result'    => $responseBody,
             ]);
 
-            throw new \RuntimeException($errorMessage, previous: $e);
+            $message = $errorMessage;
+            $errors  = $responseBody['errors'] ?? [];
+
+            if ($errors) {
+                $details = [];
+
+                foreach ($errors as $field => $msgs) {
+                    foreach ((array) $msgs as $msg) {
+                        $details[] = $msg;
+                    }
+                }
+
+                if ($details) {
+                    $message .= ': '.implode(' ', $details);
+                }
+            }
+
+            throw new \RuntimeException($message, previous: $e);
         }
     }
 

+ 12 - 3
app/Services/Pagarme/PagarmeCardService.php

@@ -32,11 +32,12 @@ class PagarmeCardService
 
         $cardData = CardResponseData::fromArray($this->pagarmeRequest(
             method: 'POST',
-            path: "/customers/{$customerId}/cards",
+            path:   "/customers/{$customerId}/cards",
 
             payload: new CardRequestData(
                 token: $paymentMethod->token,
                 label: $paymentMethod->card_name,
+
                 billingAddress: CardBillingAddressData::fromAddress(
                     Address::query()
                         ->with(['city.state', 'state'])
@@ -49,7 +50,7 @@ class PagarmeCardService
             ),
 
             idempotencyKey: $this->idempotencyKey($paymentMethod),
-            errorMessage: 'Erro ao salvar cartao no Pagar.me.',
+            errorMessage:   'Erro ao salvar cartao no Pagar.me.',
         ));
 
         $cardId = $cardData->requireId();
@@ -67,6 +68,14 @@ class PagarmeCardService
 
     private function idempotencyKey(ClientPaymentMethod $paymentMethod): string
     {
-        return "client-payment-method-{$paymentMethod->id}-card";
+        if (! empty($paymentMethod->idempotency_key)) {
+            return $paymentMethod->idempotency_key;
+        }
+
+        $key = 'card-'.(string) \Illuminate\Support\Str::uuid();
+
+        $paymentMethod->forceFill(['idempotency_key' => $key])->save();
+
+        return $key;
     }
 }

+ 39 - 47
app/Services/Pagarme/PagarmeCustomerService.php

@@ -8,55 +8,57 @@ use App\Data\Pagarme\Request\CustomerRequestData\CustomerPhonesRequestData\Custo
 use App\Data\Pagarme\Request\CustomerRequestData\CustomerRequestData;
 use App\Data\Pagarme\Response\CustomerResponseData\CustomerResponseData;
 use App\Models\Client;
+use App\Services\Pagarme\Concerns\FormatsPagarmeData;
 use App\Services\Pagarme\Concerns\SendsPagarmeRequests;
-use Illuminate\Support\Str;
 
 class PagarmeCustomerService
 {
+    use FormatsPagarmeData;
     use SendsPagarmeRequests;
 
     public function createCustomerForClient(Client $client, array $data): ?string
     {
-        if (! empty($client->external_customer_id)) {
-            return $client->external_customer_id;
+        if (! empty($client->gateway_customer_id)) {
+            return $client->gateway_customer_id;
         }
 
         $client->loadMissing('user');
 
-        $name = $client->user?->name ?? $data['name'] ?? 'Cliente';
+        $name  = $client->user?->name ?? $data['name'] ?? 'Cliente';
         $email = $client->user?->email ?? $data['email'] ?? null;
-        $code = $this->ensureCustomerCode($client);
+        $code  = $client->ensureGatewayCode();
+
+        $document = $this->digits($client->document ?? $data['document'] ?? null);
 
-        $document = $this->onlyDigits($client->document ?? $data['document'] ?? null);
         $documentLen = strlen($document);
 
         $address = $this->buildAddressData($data);
-        $phones = $this->buildPhones($client->user?->phone ?? $data['phone'] ?? null);
+        $phones  = $this->buildPhones($client->user?->phone ?? $data['phone'] ?? null);
 
         $customerRequest = new CustomerRequestData(
-            name: $name,
-            email: (string) $email,
-            document: $document,
-            type: $documentLen === 14 ? 'company' : 'individual',
+            name:         $name,
+            email:        (string) $email,
+            document:     $document,
+            type:         $documentLen === 14 ? 'company' : 'individual',
             documentType: $documentLen === 14 ? 'CNPJ' : 'CPF',
-            code: $code,
-            address: $address,
-            phones: $phones,
+            code:         $code,
+            address:      $address,
+            phones:       $phones,
         );
 
         $customerData = CustomerResponseData::fromArray($this->pagarmeRequest(
-            method: 'POST',
-            path: '/customers',
-            payload: $customerRequest,
-            idempotencyKey: $this->idempotencyKey($client->id),
-            errorMessage: 'Erro ao criar cliente no Pagar.me.',
+            method:         'POST',
+            path:           '/customers',
+            payload:        $customerRequest,
+            idempotencyKey: $this->idempotencyKey($client),
+            errorMessage:   'Erro ao criar cliente no Pagar.me.',
         ));
 
         $customerId = $customerData->requireId();
 
         $client->forceFill([
-            'external_customer_id'   => $customerId,
-            'external_customer_code' => $code,
+            'gateway_customer_id'   => $customerId,
+            'gateway_customer_code' => $code,
         ])->save();
 
         return $customerId;
@@ -73,62 +75,52 @@ class PagarmeCustomerService
         ], static fn ($value) => $value !== '');
 
         return new CustomerAddressRequestData(
-            line1: implode(', ', $line1Parts),
-            line2: (string) ($data['complement'] ?? $data['instructions'] ?? ''),
-            zipCode: $this->onlyDigits($data['zip_code'] ?? null),
-            city: (string) ($data['city'] ?? ''),
-            state: (string) ($data['state'] ?? ''),
+            line1:   implode(', ', $line1Parts),
+            line2:   (string) ($data['complement'] ?? $data['instructions'] ?? ''),
+            zipCode: $this->digits($data['zip_code'] ?? null),
+            city:    (string) ($data['city'] ?? ''),
+            state:   (string) ($data['state'] ?? ''),
             country: (string) ($data['country'] ?? 'BR'),
         );
     }
 
     private function buildPhones(?string $phone): CustomerPhonesRequestData
     {
-        $digits = $this->onlyDigits($phone);
+        $digits = $this->digits($phone);
 
         if ($digits === '') {
             return new CustomerPhonesRequestData;
         }
 
         $areaCode = substr($digits, 0, 2);
-        $number = substr($digits, 2);
+        $number   = substr($digits, 2);
 
         if (strlen($digits) <= 2) {
             $areaCode = '';
-            $number = $digits;
+            $number   = $digits;
         }
 
         return new CustomerPhonesRequestData(
             mobilePhone: new CustomerPhoneData(
                 countryCode: '55',
-                areaCode: $areaCode,
-                number: $number,
+                areaCode:    $areaCode,
+                number:      $number,
             ),
         );
     }
 
-    private function onlyDigits(?string $value): string
-    {
-        return preg_replace('/\D+/', '', (string) $value) ?? '';
-    }
-
     // evita criacao duplicada de customer
 
-    private function idempotencyKey(int $clientId, string $suffix = 'customer'): string
-    {
-        return "client-{$clientId}-{$suffix}";
-    }
-
-    private function ensureCustomerCode(Client $client): string
+    private function idempotencyKey(Client $client): string
     {
-        if (! empty($client->external_customer_code)) {
-            return $client->external_customer_code;
+        if (! empty($client->idempotency_key)) {
+            return $client->idempotency_key;
         }
 
-        $code = 'client-'.(string) Str::uuid();
+        $key = 'customer-'.(string) \Illuminate\Support\Str::uuid();
 
-        $client->forceFill(['external_customer_code' => $code])->save();
+        $client->forceFill(['idempotency_key' => $key])->save();
 
-        return $code;
+        return $key;
     }
 }

+ 156 - 117
app/Services/Pagarme/PagarmePaymentService.php

@@ -9,37 +9,40 @@ use App\Data\Pagarme\Request\CustomerRequestData\CustomerRequestData;
 use App\Data\Pagarme\Request\OrderRequestData\OrderItemData;
 use App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData\OrderCreditCardData;
 use App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData\OrderPaymentData;
-use App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData\OrderPixAdditionalInformationData;
-use App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData\OrderPixData;
-use App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData\OrderSplitData;
-use App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData\OrderSplitOptionsData;
+use App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData\OrderPixData\OrderPixAdditionalInformationData;
+use App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData\OrderPixData\OrderPixData;
+use App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData\OrderSplitData\OrderSplitData;
+use App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData\OrderSplitData\OrderSplitOptionsData;
 use App\Data\Pagarme\Request\OrderRequestData\OrderRequestData;
 use App\Data\Pagarme\Response\OrderResponseData\OrderResponseData;
 use App\Enums\PaymentSplitStatusEnum;
 use App\Enums\PaymentStatusEnum;
 use App\Models\Address;
+use App\Models\Client;
 use App\Models\Payment;
 use App\Models\PaymentSplit;
 use App\Models\Schedule;
+use App\Services\Pagarme\Concerns\FormatsPagarmeData;
 use App\Services\Pagarme\Concerns\SendsPagarmeRequests;
 use Illuminate\Support\Str;
 
 class PagarmePaymentService
 {
+    use FormatsPagarmeData;
     use SendsPagarmeRequests;
 
     public function processPayment(
-        Payment $payment,
+        Payment  $payment,
         Schedule $schedule,
-        string $paymentMethod,
-        ?string $cardId = null,
-        array $options = [],
+        string   $paymentMethod,
+        ?string  $cardId  = null,
+        array    $options = [],
     ): array {
         $grossAmount = (float) $payment->gross_amount;
 
-        $items = $this->buildOrderItems($schedule, $grossAmount);
+        $items    = $this->buildOrderItems($schedule, $grossAmount);
         $customer = $this->buildCustomer($schedule, $options);
-        $split = $this->buildSplit($payment, $options);
+        $split    = $this->buildSplit($payment, $options);
 
         $pixOptions = config('services.pagarme.pix_disable_split')
             ? []
@@ -49,84 +52,89 @@ class PagarmePaymentService
 
         if ($paymentMethod === 'credit_card') {
             $creditCard = new OrderCreditCardData(
-                cardId: $cardId,
-                installments: 1,
+                cardId:              $cardId,
+                installments:        1,
                 statementDescriptor: Str::limit((string) config('app.name', 'SOFTPAR'), 13, ''),
-                operationType: 'auth_and_capture',
+                operationType:       'auth_and_capture',
             );
 
             return $this->createOrderWithCreditCard(
-                payment: $payment,
-                items: $items,
-                customer: $customer,
+                payment:    $payment,
+                items:      $items,
+                customer:   $customer,
                 creditCard: $creditCard,
-                options: $orderOptions,
+                options:    $orderOptions,
             );
         }
 
         $pixData = new OrderPixData(
             expiresIn: 1800,
+
             additionalInformation: [
                 new OrderPixAdditionalInformationData(
-                    name: 'Agendamento',
+                    name:  'Agendamento',
                     value: (string) $schedule->id,
                 ),
             ],
         );
 
         return $this->createOrderWithPix(
-            payment: $payment,
-            items: $items,
+            payment:  $payment,
+            items:    $items,
             customer: $customer,
-            pix: $pixData,
-            options: $pixOptions,
+            pix:      $pixData,
+            options:  $pixOptions,
         );
     }
 
     public function createOrderWithCreditCard(
-        Payment $payment,
-        array $items,
+        Payment             $payment,
+        array               $items,
         CustomerRequestData $customer,
         OrderCreditCardData $creditCard,
-        array $options = []
+        array               $options = []
     ): array {
         return $this->createOrder(
-            payment: $payment,
-            items: $items,
+            payment:  $payment,
+            items:    $items,
             customer: $customer,
+
             paymentMethod: OrderRequestData::creditCardPaymentMethod(
                 creditCard: $creditCard,
-                split: is_array($options['split'] ?? null) ? $options['split'] : null,
+                split:      is_array($options['split'] ?? null) ? $options['split'] : null,
             ),
+
             options: $options,
         );
     }
 
     public function createOrderWithPix(
-        Payment $payment,
-        array $items,
+        Payment             $payment,
+        array               $items,
         CustomerRequestData $customer,
-        OrderPixData $pix,
-        array $options = []
+        OrderPixData        $pix,
+        array               $options = []
     ): array {
         return $this->createOrder(
-            payment: $payment,
-            items: $items,
+            payment:  $payment,
+            items:    $items,
             customer: $customer,
+
             paymentMethod: OrderRequestData::pixPaymentMethod(
-                pix: $pix,
+                pix:   $pix,
                 split: is_array($options['split'] ?? null) ? $options['split'] : null,
             ),
+
             options: $options,
         );
     }
 
     public function createOrder(
-        Payment $payment,
-        array $items,
+        Payment             $payment,
+        array               $items,
         CustomerRequestData $customer,
-        OrderPaymentData $paymentMethod,
-        array $options = []
+        OrderPaymentData    $paymentMethod,
+        array               $options = []
     ): array {
         $metadata = array_merge([
             'payment_id'  => (string) $payment->id,
@@ -136,26 +144,28 @@ class PagarmePaymentService
         ], $options['metadata'] ?? []);
 
         $requestData = new OrderRequestData(
-            code: $this->ensurePaymentCode($payment),
-            items: $items,
-            payments: [$paymentMethod],
-            metadata: $metadata,
-            customer: $customer,
+            code:       $payment->ensureGatewayCode(),
+            items:      $items,
+            payments:   [$paymentMethod],
+            metadata:   $metadata,
+            customer:   $customer,
             customerId: $options['customer_id'] ?? null,
-            closed: $options['closed'] ?? true,
-            channel: $options['channel'] ?? null,
+            closed:     $options['closed']      ?? true,
+            channel:    $options['channel']     ?? null,
         );
 
         $order = OrderResponseData::fromArray($this->pagarmeRequest(
-            method: 'POST',
-            path: '/orders',
-            payload: $requestData,
+            method:         'POST',
+            path:           '/orders',
+            payload:        $requestData,
             idempotencyKey: $this->idempotencyKey($payment),
-            errorMessage: 'Erro ao criar pedido de pagamento no Pagar.me.',
+            errorMessage:   'Erro ao criar pedido de pagamento no Pagar.me.',
         ));
 
         $order->requireId();
 
+        $this->saveExternalCustomerId($payment, $order);
+
         return $order->toArray();
     }
 
@@ -164,15 +174,19 @@ class PagarmePaymentService
     public function applyGatewayResponseToPayment(Payment $payment, array $orderResponse): Payment
     {
         $order = OrderResponseData::fromArray($orderResponse);
-        $newStatus = $order->paymentStatus();
-        $failureCode = null;
+
+        $newStatus      = $order->paymentStatus();
+        $failureCode    = null;
         $failureMessage = null;
 
         if ($newStatus === PaymentStatusEnum::FAILED) {
-            $failureCode = $order->failureCode();
+            $failureCode    = $order->failureCode();
             $failureMessage = $order->failureMessage();
         }
 
+        $gatewayFeeCents = $order->lastTransaction()?->cost ?? 0;
+        $gatewayFee      = $gatewayFeeCents > 0 ? round($gatewayFeeCents / 100, 2) : 0;
+
         $payment->forceFill([
             'gateway_provider'            => 'pagarme',
             'gateway_entity_reference'    => $order->gatewayEntityReference(),
@@ -183,6 +197,7 @@ class PagarmePaymentService
             'paid_at'                     => $order->paidAt(),
             'authorized_at'               => $order->authorizedAt(),
             'gateway_payload'             => $orderResponse,
+            'gateway_fee_amount'          => $gatewayFee,
             'failure_code'                => $failureCode,
             'failure_message'             => $failureMessage,
         ])->save();
@@ -204,37 +219,10 @@ class PagarmePaymentService
 
     //
 
-    public function ensureCustomerPhone(Schedule $schedule, array $options): void
-    {
-        $phone = $this->buildPhonePayload($schedule->client?->user?->phone)
-            ?: $this->buildPhonePayload($options['phone'] ?? null);
-
-        if (! $phone) {
-            throw new \InvalidArgumentException(
-                'Voce precisa cadastrar um numero de celular valido no seu perfil para concluir o pagamento.'
-            );
-        }
-    }
-
-    //
-
-    private function buildOrderItems(Schedule $schedule, float $grossAmount): array
-    {
-        $description = $schedule->customSchedule?->serviceType?->description
-            ?? "Servico {$schedule->id}";
-
-        return [new OrderItemData(
-            code: "schedule-{$schedule->id}",
-            amount: OrderRequestData::amountInCents($grossAmount),
-            quantity: 1,
-            description: $description,
-        )];
-    }
-
     private function buildCustomer(Schedule $schedule, array $options = []): CustomerRequestData
     {
-        $client = $schedule->client;
-        $user = $client->user()->first(['id', 'name', 'email', 'phone']);
+        $client  = $schedule->client;
+        $user    = $client->user()->first(['id', 'name', 'email', 'phone']);
         $address = Address::with(['city.state', 'state'])->find($schedule->address_id);
 
         foreach ([
@@ -256,8 +244,8 @@ class PagarmePaymentService
         $phone = $this->buildPhonePayload($user->phone)
             ?: $this->buildPhonePayload($options['phone'] ?? null);
 
-        $state = $address->state?->code ?? $address->city?->state?->code;
-        $city = $address->city?->name;
+        $state   = $address->state?->code ?? $address->city?->state?->code;
+        $city    = $address->city?->name;
         $zipCode = $this->digits($address->zip_code);
 
         $line1 = implode(', ', array_filter([
@@ -280,11 +268,11 @@ class PagarmePaymentService
         }
 
         $customerAddress = new CustomerAddressRequestData(
-            line1: $line1,
-            line2: $address->complement ?: $address->instructions,
+            line1:   $line1,
+            line2:   $address->complement ?: $address->instructions,
             zipCode: $zipCode,
-            city: $city,
-            state: $state,
+            city:    $city,
+            state:   $state,
             country: 'BR',
         );
 
@@ -294,24 +282,37 @@ class PagarmePaymentService
             $customerPhones = new CustomerPhonesRequestData(
                 mobilePhone: new CustomerPhoneData(
                     countryCode: $phone['country_code'],
-                    areaCode: $phone['area_code'],
-                    number: $phone['number'],
+                    areaCode:    $phone['area_code'],
+                    number:      $phone['number'],
                 ),
             );
         }
 
         return new CustomerRequestData(
-            name: $user->name,
-            email: $user->email,
-            document: $document,
-            type: strlen($document) === 14 ? 'company' : 'individual',
+            name:         $user->name,
+            email:        $user->email,
+            document:     $document,
+            type:         strlen($document) === 14 ? 'company' : 'individual',
             documentType: strlen($document) === 14 ? 'CNPJ' : 'CPF',
-            code: "client-{$client->id}",
-            address: $customerAddress,
-            phones: $customerPhones,
+            code:         $client->ensureGatewayCode(),
+            address:      $customerAddress,
+            phones:       $customerPhones,
         );
     }
 
+    private function buildOrderItems(Schedule $schedule, float $grossAmount): array
+    {
+        $description = $schedule->customSchedule?->serviceType?->description
+            ?? "Servico {$schedule->id}";
+
+        return [new OrderItemData(
+            code:        "schedule-{$schedule->id}",
+            amount:      OrderRequestData::amountInCents($grossAmount),
+            quantity:    1,
+            description: $description,
+        )];
+    }
+
     private function buildPhonePayload(?string $phone): ?array
     {
         $digits = $this->digits($phone);
@@ -339,19 +340,31 @@ class PagarmePaymentService
 
         $split = OrderRequestData::splitFromTransfers($transfers);
 
-        $platformFee = (float) ($payment->platform_fee_amount ?? 0);
+        $platformRecipientId = config('services.pagarme.platform_recipient_id');
+
+        if (empty($platformRecipientId)) {
+            return $split;
+        }
+
+        $orderAmountCents = OrderRequestData::amountInCents((float) $payment->gross_amount);
+
+        $providerTotalCents = array_sum(array_map(
+            static fn (OrderSplitData $s) => $s->amount,
+            $split,
+        ));
 
-        if ($platformFee > 0) {
-            $platformRecipientId = config('services.pagarme.platform_recipient_id');
+        $platformAmountCents = $orderAmountCents - $providerTotalCents;
 
+        if ($platformAmountCents > 0) {
             $split[] = new OrderSplitData(
-                amount: OrderRequestData::amountInCents($platformFee),
+                amount:      $platformAmountCents,
                 recipientId: $platformRecipientId,
-                type: 'flat',
+                type:        'flat',
+
                 options: new OrderSplitOptionsData(
                     chargeProcessingFee: true,
-                    chargeRemainderFee: true,
-                    liable: true,
+                    chargeRemainderFee:  true,
+                    liable:              true,
                 ),
             );
         }
@@ -359,28 +372,54 @@ class PagarmePaymentService
         return $split;
     }
 
-    private function digits(?string $value): string
-    {
-        return preg_replace('/\D+/', '', (string) $value) ?? '';
-    }
-
-    //
+    // evita criacao duplicada de payment
 
     private function idempotencyKey(Payment $payment): string
     {
-        return "payment-{$payment->id}-schedule-{$payment->schedule_id}";
+        if (! empty($payment->idempotency_key)) {
+            return $payment->idempotency_key;
+        }
+
+        $key = 'order-'.(string) \Illuminate\Support\Str::uuid();
+
+        $payment->forceFill(['idempotency_key' => $key])->save();
+
+        return $key;
     }
 
-    private function ensurePaymentCode(Payment $payment): string
+    // salva o gateway_customer_id do Pagar.me no Client apos criacao de ordem
+
+    private function saveExternalCustomerId(Payment $payment, OrderResponseData $order): void
     {
-        if (! empty($payment->gateway_code)) {
-            return $payment->gateway_code;
+        $customerId   = $order->customer?->id;
+        $customerCode = $order->customer?->code;
+
+        if (! $customerId && ! $customerCode) {
+            return;
+        }
+
+        $client = Client::find($payment->client_id);
+
+        if (! $client) {
+            return;
+        }
+
+        $updated = false;
+
+        if (! $client->gateway_customer_id && $customerId) {
+            $client->gateway_customer_id = $customerId;
+
+            $updated = true;
         }
 
-        $code = 'payment-'.(string) Str::uuid();
+        if (! $client->gateway_customer_code && $customerCode) {
+            $client->gateway_customer_code = $customerCode;
 
-        $payment->forceFill(['gateway_code' => $code])->save();
+            $updated = true;
+        }
 
-        return $code;
+        if ($updated) {
+            $client->save();
+        }
     }
 }

+ 64 - 150
app/Services/Pagarme/PagarmeRecipientService.php

@@ -3,22 +3,22 @@
 namespace App\Services\Pagarme;
 
 use App\Data\Pagarme\Request\BankAccountUpdateRequestData;
-use App\Data\Pagarme\Request\RecipientRequestData\RecipientAddressData;
 use App\Data\Pagarme\Request\RecipientRequestData\RecipientAutomaticAnticipationSettingsData;
 use App\Data\Pagarme\Request\RecipientRequestData\RecipientBankAccountData;
-use App\Data\Pagarme\Request\RecipientRequestData\RecipientPhoneData;
-use App\Data\Pagarme\Request\RecipientRequestData\RecipientPhoneNumbersData;
-use App\Data\Pagarme\Request\RecipientRequestData\RecipientRegisterInformationData;
+use App\Data\Pagarme\Request\RecipientRequestData\RecipientRegisterInformationData\RecipientAddressData;
+use App\Data\Pagarme\Request\RecipientRequestData\RecipientRegisterInformationData\RecipientPhoneNumbersData\RecipientPhoneData;
+use App\Data\Pagarme\Request\RecipientRequestData\RecipientRegisterInformationData\RecipientPhoneNumbersData\RecipientPhoneNumbersData;
+use App\Data\Pagarme\Request\RecipientRequestData\RecipientRegisterInformationData\RecipientRegisterInformationData;
 use App\Data\Pagarme\Request\RecipientRequestData\RecipientRequestData;
 use App\Data\Pagarme\Request\RecipientRequestData\RecipientTransferSettingsData;
 use App\Data\Pagarme\Response\RecipientResponseData\RecipientResponseData;
 use App\Models\Provider;
+use App\Services\Pagarme\Concerns\FormatsPagarmeData;
 use App\Services\Pagarme\Concerns\SendsPagarmeRequests;
-use Carbon\Carbon;
-use Illuminate\Support\Str;
 
 class PagarmeRecipientService
 {
+    use FormatsPagarmeData;
     use SendsPagarmeRequests;
 
     public function createRecipientForProvider(Provider $provider, array $data): string
@@ -27,19 +27,19 @@ class PagarmeRecipientService
             return $provider->recipient_id;
         }
 
-        $metadata = $data['recipient_metadata'] ?? [];
-        $paymentMode = $data['recipient_payment_mode'];
-        $recipientCode = $this->ensureRecipientCode($provider);
+        $metadata      = $data['recipient_metadata'] ?? [];
+        $paymentMode   = $data['recipient_payment_mode'];
+        $recipientCode = $provider->ensureGatewayCode();
 
         $addressParts = $this->extractAddressParts($data);
 
         $registerInformation = new RecipientRegisterInformationData(
-            name: $data['recipient_name'],
-            email: $data['recipient_email'],
-            document: $this->onlyDigits($data['recipient_document'] ?? null),
-            type: $data['recipient_type'] ?? 'individual',
-            birthdate: $this->formatBirthdate($data['birth_date'] ?? null),
-            monthlyIncome: isset($data['monthly_income']) ? (int) $data['monthly_income'] : 1000,
+            name:                   $data['recipient_name'],
+            email:                  $data['recipient_email'],
+            document:               $this->digits($data['recipient_document'] ?? null),
+            type:                   $data['recipient_type'] ?? 'individual',
+            birthdate:              $this->formatBirthdate($data['birth_date'] ?? null),
+            monthlyIncome:          isset($data['monthly_income']) ? (int) $data['monthly_income'] : 1000,
             professionalOccupation: $data['professional_occupation'] ?? 'autonomo',
 
             phoneNumbers: new RecipientPhoneNumbersData(
@@ -47,13 +47,13 @@ class PagarmeRecipientService
             ),
 
             address: new RecipientAddressData(
-                street: $data['address'],
-                complementary: $addressParts['complementary'],
-                streetNumber: $addressParts['street_number'],
-                neighborhood: $addressParts['neighborhood'],
-                city: $data['city'] ?? null,
-                state: $data['state'] ?? null,
-                zipCode: $this->onlyDigits($data['zip_code'] ?? null),
+                street:         $addressParts['street'],
+                complementary:  $addressParts['complementary'],
+                streetNumber:   $addressParts['street_number'],
+                neighborhood:   $addressParts['neighborhood'],
+                city:           $data['city']  ?? null,
+                state:          $data['state'] ?? null,
+                zipCode:        $this->digits($data['zip_code'] ?? null),
                 referencePoint: $addressParts['reference_point'],
             ),
         );
@@ -66,12 +66,12 @@ class PagarmeRecipientService
             code: $recipientCode,
 
             registerInformation: $registerInformation,
-            defaultBankAccount: $defaultBankAccount,
+            defaultBankAccount:  $defaultBankAccount,
 
             transferSettings: new RecipientTransferSettingsData(
-                transferEnabled: false,
+                transferEnabled:  false,
                 transferInterval: 'Daily',
-                transferDay: 0,
+                transferDay:      0,
             ),
 
             automaticAnticipationSettings: new RecipientAutomaticAnticipationSettingsData(
@@ -82,11 +82,11 @@ class PagarmeRecipientService
         $bankAccountData = $payload->defaultBankAccount->toArray();
 
         $raw = $this->pagarmeRequest(
-            method: 'POST',
-            path: '/recipients',
-            payload: $payload,
-            idempotencyKey: $this->idempotencyKey($provider->id),
-            errorMessage: 'Erro ao criar recebedor no Pagar.me.',
+            method:         'POST',
+            path:           '/recipients',
+            payload:        $payload,
+            idempotencyKey: $this->idempotencyKey($provider),
+            errorMessage:   'Erro ao criar recebedor no Pagar.me.',
         );
 
         $recipientData = RecipientResponseData::fromArray($raw);
@@ -97,7 +97,7 @@ class PagarmeRecipientService
             'recipient_id'                   => $recipientId,
             'recipient_name'                 => $data['recipient_name'],
             'recipient_email'                => $data['recipient_email'],
-            'recipient_description'          => $data['recipient_description'],
+            'recipient_description'          => $data['recipient_description'] ?? '',
             'recipient_document'             => $data['recipient_document'],
             'recipient_type'                 => $payload->registerInformation->type,
             'recipient_code'                 => $recipientCode,
@@ -123,23 +123,23 @@ class PagarmeRecipientService
     public function updateDefaultBankAccount(Provider $provider, array $bankAccountData): Provider
     {
         $payload = new BankAccountUpdateRequestData(
-            holderName: $this->normalizeHolderName($bankAccountData['holder_name']),
-            holderType: $bankAccountData['holder_type'],
-            holderDocument: $this->onlyDigits($bankAccountData['holder_document']),
-            bank: $bankAccountData['bank'],
-            branchNumber: $bankAccountData['branch_number'],
-            branchCheckDigit: $bankAccountData['branch_check_digit'] ?? null,
-            accountNumber: $bankAccountData['account_number'],
+            holderName:        $this->normalizeHolderName($bankAccountData['holder_name']),
+            holderType:        $bankAccountData['holder_type'],
+            holderDocument:    $this->digits($bankAccountData['holder_document']),
+            bank:              $bankAccountData['bank'],
+            branchNumber:      $bankAccountData['branch_number'],
+            branchCheckDigit:  $bankAccountData['branch_check_digit'] ?? null,
+            accountNumber:     $bankAccountData['account_number'],
             accountCheckDigit: $bankAccountData['account_check_digit'],
-            type: $bankAccountData['type'],
+            type:              $bankAccountData['type'],
         );
 
         $raw = $this->pagarmeRequest(
-            method: 'PATCH',
-            path: "/recipients/{$provider->recipient_id}/default-bank-account",
-            payload: $payload,
-            idempotencyKey: $this->idempotencyKey($provider->id, 'default-bank-account-'.sha1(json_encode($payload->toArray()))),
-            errorMessage: 'Erro ao atualizar conta bancaria do recebedor no Pagar.me.',
+            method:         'PATCH',
+            path:           "/recipients/{$provider->recipient_id}/default-bank-account",
+            payload:        $payload,
+            idempotencyKey: $this->idempotencyKey($provider, 'default-bank-account-'.sha1(json_encode($payload->toArray()))),
+            errorMessage:   'Erro ao atualizar conta bancaria do recebedor no Pagar.me.',
         );
 
         $recipientData = RecipientResponseData::fromArray($raw);
@@ -156,27 +156,27 @@ class PagarmeRecipientService
     private function buildRecipientBankAccount(array $data): RecipientBankAccountData
     {
         return new RecipientBankAccountData(
-            holderName: $this->normalizeHolderName($data['holder_name']),
-            holderType: $data['holder_type'],
-            holderDocument: $this->onlyDigits($data['holder_document']),
-            bank: $data['bank'],
-            branchNumber: $data['branch_number'],
-            branchCheckDigit: $data['branch_check_digit'] ?? null,
-            accountNumber: $data['account_number'],
+            holderName:        $this->normalizeHolderName($data['holder_name']),
+            holderType:        $data['holder_type'],
+            holderDocument:    $this->digits($data['holder_document']),
+            bank:              $data['bank'],
+            branchNumber:      $data['branch_number'],
+            branchCheckDigit:  $data['branch_check_digit'] ?? null,
+            accountNumber:     $data['account_number'],
             accountCheckDigit: $data['account_check_digit'],
-            type: $data['type'],
+            type:              $data['type'],
         );
     }
 
     private function buildRecipientPhone(?string $phone): RecipientPhoneData
     {
-        $digits = $this->onlyDigits($phone);
+        $digits = $this->digits($phone);
 
         if (strlen($digits) < 10) {
             return new RecipientPhoneData(
-                ddd: '11',
+                ddd:    '11',
                 number: '999999999',
-                type: 'mobile',
+                type:   'mobile',
             );
         }
 
@@ -185,110 +185,24 @@ class PagarmeRecipientService
         }
 
         return new RecipientPhoneData(
-            ddd: substr($digits, 0, 2),
+            ddd:    substr($digits, 0, 2),
             number: substr($digits, 2),
-            type: 'mobile',
+            type:   'mobile',
         );
     }
 
-    private 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 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 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, '');
-    }
-
-    private function onlyDigits(?string $value): string
-    {
-        return preg_replace('/\D+/', '', (string) $value) ?? '';
-    }
-
     // evita criacao duplica de recipient
 
-    private function idempotencyKey(int $providerId, string $suffix = 'recipient'): string
+    private function idempotencyKey(Provider $provider, string $suffix = ''): string
     {
-        return "provider-{$providerId}-{$suffix}";
-    }
+        $baseKey = $provider->idempotency_key;
 
-    private function ensureRecipientCode(Provider $provider): string
-    {
-        if (! empty($provider->recipient_code)) {
-            return $provider->recipient_code;
-        }
+        if (empty($baseKey)) {
+            $baseKey = 'recipient-'.(string) \Illuminate\Support\Str::uuid();
 
-        $recipientCode = 'provider-'.(string) Str::uuid();
-
-        $provider->forceFill(['recipient_code' => $recipientCode])->save();
+            $provider->forceFill(['idempotency_key' => $baseKey])->save();
+        }
 
-        return $recipientCode;
+        return $suffix ? "{$baseKey}-{$suffix}" : $baseKey;
     }
 }

+ 63 - 44
app/Services/Pagarme/PagarmeTransferService.php

@@ -17,21 +17,23 @@ class PagarmeTransferService
     {
         if ($this->shouldMockTransferRequest()) {
             return $this->mockTransferResponse(
-                amountInCents: $amountInCents,
-                recipientId: $recipientId,
+                amountInCents:  $amountInCents,
+                recipientId:    $recipientId,
                 idempotencyKey: $idempotencyKey,
             );
         }
 
         return TransferResponseData::fromArray($this->pagarmeRequest(
             method: 'POST',
-            path: '/transfers',
+            path:   '/transfers',
+
             payload: new TransferRequestData(
-                amount: $amountInCents,
+                amount:      $amountInCents,
                 recipientId: $recipientId,
             ),
+
             idempotencyKey: $idempotencyKey,
-            errorMessage: 'Erro ao criar transferencia (saque) no Pagar.me.',
+            errorMessage:   'Erro ao criar transferencia (saque) no Pagar.me.',
         ));
     }
 
@@ -42,47 +44,41 @@ class PagarmeTransferService
         }
 
         return TransferResponseData::fromArray($this->pagarmeRequest(
-            method: 'GET',
-            path: "/transfers/{$transferId}",
-            payload: [],
+            method:         'GET',
+            path:           "/transfers/{$transferId}",
+            payload:        [],
             idempotencyKey: "get-transfer-{$transferId}",
-            errorMessage: 'Erro ao consultar transferencia no Pagar.me.',
+            errorMessage:   'Erro ao consultar transferencia no Pagar.me.',
         ));
     }
 
     //
 
-    private function shouldMockTransferRequest(): bool
+    public function getRecipientBalance(string $recipientId): array
     {
-        return app()->environment('local', 'development');
+        return $this->pagarmeRequest(
+            method:         'GET',
+            path:           "/recipients/{$recipientId}/balance",
+            payload:        [],
+            idempotencyKey: "get-recipient-balance-{$recipientId}",
+            errorMessage:   'Erro ao consultar saldo do recebedor no Pagar.me.',
+        );
     }
 
-    private function mockTransferResponse(int $amountInCents, string $recipientId, string $idempotencyKey): TransferResponseData
+    //
+
+    private function mockTransferId(string $idempotencyKey): string
     {
-        $provider = Provider::query()->where('recipient_id', $recipientId)->first();
-        $createdAt = Carbon::now();
+        return (string) abs(crc32($idempotencyKey));
+    }
 
-        return new TransferResponseData(
-            id: $this->mockTransferId($idempotencyKey),
-            amount: $amountInCents,
-            type: 'credito_em_conta',
-            status: 'pending_transfer',
-            fee: 0,
-            fundingDate: null,
-            fundingEstimatedDate: $createdAt->copy()->addWeekday()->toISOString(),
-            bankAccount: $provider?->recipient_default_bank_account,
-            bankResponse: null,
-            createdAt: $createdAt->toISOString(),
-            metadata: [
-                'mocked'          => true,
-                'environment'     => app()->environment(),
-                'recipient_id'    => $recipientId,
-                'idempotency_key' => $idempotencyKey,
-                'provider_id'     => $provider?->id,
-            ],
-        );
+    private function shouldMockTransferRequest(): bool
+    {
+        return app()->environment('local', 'development');
     }
 
+    //
+
     private function mockTransferLookupResponse(string $transferId): TransferResponseData
     {
         $withdrawal = ProviderWithdrawal::query()->where('transfer_id', $transferId)->first();
@@ -94,16 +90,17 @@ class PagarmeTransferService
         $createdAt = Carbon::now();
 
         return new TransferResponseData(
-            id: $transferId,
-            amount: $withdrawal ? (int) round((float) $withdrawal->gross_amount * 100) : 0,
-            type: $withdrawal?->type ?? 'credito_em_conta',
-            status: $withdrawal?->status?->value ?? 'pending_transfer',
-            fee: $withdrawal ? (int) round((float) $withdrawal->gateway_fee_amount * 100) : 0,
-            fundingDate: $withdrawal?->completed_at?->toISOString(),
+            id:                   $transferId,
+            amount:               $withdrawal ? (int) round((float) $withdrawal->gross_amount * 100) : 0,
+            type:                 $withdrawal?->type ?? 'credito_em_conta',
+            status:               $withdrawal?->status?->value ?? 'pending_transfer',
+            fee:                  $withdrawal ? (int) round((float) $withdrawal->gateway_fee_amount * 100) : 0,
+            fundingDate:          $withdrawal?->completed_at?->toISOString(),
             fundingEstimatedDate: $createdAt->copy()->addWeekday()->toISOString(),
-            bankAccount: $withdrawal?->bank_account,
-            bankResponse: $withdrawal?->bank_response,
-            createdAt: $withdrawal?->created_at?->toISOString() ?? $createdAt->toISOString(),
+            bankAccount:          $withdrawal?->bank_account,
+            bankResponse:         $withdrawal?->bank_response,
+            createdAt:            $withdrawal?->created_at?->toISOString() ?? $createdAt->toISOString(),
+
             metadata: $withdrawal?->metadata ?? [
                 'mocked'      => true,
                 'environment' => app()->environment(),
@@ -111,8 +108,30 @@ class PagarmeTransferService
         );
     }
 
-    private function mockTransferId(string $idempotencyKey): string
+    private function mockTransferResponse(int $amountInCents, string $recipientId, string $idempotencyKey): TransferResponseData
     {
-        return (string) abs(crc32($idempotencyKey));
+        $provider  = Provider::query()->where('recipient_id', $recipientId)->first();
+        $createdAt = Carbon::now();
+
+        return new TransferResponseData(
+            id:                   $this->mockTransferId($idempotencyKey),
+            amount:               $amountInCents,
+            type:                 'credito_em_conta',
+            status:               'pending_transfer',
+            fee:                  0,
+            fundingDate:          null,
+            fundingEstimatedDate: $createdAt->copy()->addWeekday()->toISOString(),
+            bankAccount:          $provider?->recipient_default_bank_account,
+            bankResponse:         null,
+            createdAt:            $createdAt->toISOString(),
+
+            metadata: [
+                'mocked'          => true,
+                'environment'     => app()->environment(),
+                'recipient_id'    => $recipientId,
+                'idempotency_key' => $idempotencyKey,
+                'provider_id'     => $provider?->id,
+            ],
+        );
     }
 }

+ 79 - 1
app/Services/PaymentService.php

@@ -111,6 +111,16 @@ class PaymentService
                     'failed_at'       => now(),
                     'failure_message' => 'Pagamento pendente sem retorno do gateway.',
                 ])->save();
+            } elseif ($this->isExpiredPixPayment($existingPayment)) {
+                $existingPayment->forceFill([
+                    'status'          => PaymentStatusEnum::FAILED,
+                    'failed_at'       => now(),
+                    'failure_message' => 'Pagamento Pix expirado.',
+                ])->save();
+
+                PaymentSplit::query()
+                    ->where('payment_id', $existingPayment->id)
+                    ->update(['status' => PaymentSplitStatusEnum::FAILED]);
             } else {
                 if ($existingPayment->payment_method !== $paymentMethod && $existingPayment->status !== PaymentStatusEnum::PAID) {
                     throw new \InvalidArgumentException('Ja existe um pagamento em andamento para este agendamento.');
@@ -123,6 +133,7 @@ class PaymentService
         }
 
         $clientPaymentMethod = null;
+
         $cardId = null;
 
         if ($paymentMethod === 'credit_card') {
@@ -150,6 +161,7 @@ class PaymentService
         }
 
         $serviceAmount = (float) $schedule->total_amount;
+
         $platformFee = round($serviceAmount * 0.11, 2);
         $grossAmount = round($serviceAmount + $platformFee, 2);
 
@@ -198,7 +210,7 @@ class PaymentService
             ],
         ]);
 
-        $this->pagarmePaymentService->ensureCustomerPhone($schedule, $options);
+        $schedule->ensureCustomerPhone($options['phone'] ?? null);
 
         try {
             $orderResponse = $this->pagarmePaymentService->processPayment(
@@ -229,8 +241,74 @@ class PaymentService
         return $payment;
     }
 
+    public function getOrCreatePixPayment(Schedule $schedule): Payment
+    {
+        $existingPayment = Payment::query()
+            ->where('schedule_id', $schedule->id)
+            ->where('payment_method', 'pix')
+            ->whereIn('status', [
+                PaymentStatusEnum::PENDING->value,
+                PaymentStatusEnum::PROCESSING->value,
+                PaymentStatusEnum::AUTHORIZED->value,
+                PaymentStatusEnum::PAID->value,
+            ])
+            ->latest('id')
+            ->first();
+
+        if ($existingPayment && $this->isExpiredPixPayment($existingPayment)) {
+            $existingPayment->forceFill([
+                'status'          => PaymentStatusEnum::FAILED,
+                'failed_at'       => Carbon::now(),
+                'failure_message' => 'Pagamento Pix expirado.',
+            ])->save();
+
+            PaymentSplit::query()
+                ->where('payment_id', $existingPayment->id)
+                ->update(['status' => PaymentSplitStatusEnum::FAILED]);
+
+            $existingPayment = null;
+        }
+
+        if ($existingPayment) {
+            if ($this->isIncompleteGatewayPayment($existingPayment)) {
+                $existingPayment->forceFill([
+                    'status'          => PaymentStatusEnum::FAILED,
+                    'failed_at'       => Carbon::now(),
+                    'failure_message' => 'Pagamento pendente sem retorno do gateway.',
+                ])->save();
+
+                PaymentSplit::query()
+                    ->where('payment_id', $existingPayment->id)
+                    ->update(['status' => PaymentSplitStatusEnum::FAILED]);
+            } else {
+                $this->syncScheduleStatusAfterPayment($schedule, $existingPayment);
+
+                return $existingPayment;
+            }
+        }
+
+        return $this->payAcceptedSchedule(
+            schedule: $schedule,
+            paymentMethod: 'pix',
+        );
+    }
+
     //
 
+    private function isExpiredPixPayment(Payment $payment): bool
+    {
+        if ($payment->payment_method !== 'pix') {
+            return false;
+        }
+
+        if ($payment->status === PaymentStatusEnum::PAID) {
+            return false;
+        }
+
+        return $payment->expires_at !== null
+            && $payment->expires_at->isPast();
+    }
+
     private function isIncompleteGatewayPayment(Payment $payment): bool
     {
         return $payment->status === PaymentStatusEnum::PENDING

+ 41 - 15
app/Services/ProviderService.php

@@ -46,7 +46,7 @@ class ProviderService
         return DB::transaction(function () use ($data) {
             $provider = Provider::create($data);
 
-            if (! empty($data['recipient_code'])) {
+            if (! empty($data['recipient_name']) && ! empty($data['recipient_default_bank_account'])) {
                 $this->pagarmeRecipientService->createRecipientForProvider($provider, $data);
             }
 
@@ -70,7 +70,9 @@ class ProviderService
                 sourceId: $model->id,
                 old: $model->profileMedia,
             );
+
             $data['profile_media_id'] = $media->id;
+
             unset($data['avatar']);
         }
 
@@ -79,19 +81,6 @@ class ProviderService
         return $model->fresh(['user', 'profileMedia']);
     }
 
-    public function updateBankAccount(int $id, array $bankAccountData): ?Provider
-    {
-        $provider = $this->findById($id);
-
-        if (! $provider) {
-            return null;
-        }
-
-        $this->pagarmeRecipientService->updateDefaultBankAccount($provider, $bankAccountData);
-
-        return $provider->fresh(['user', 'profileMedia']);
-    }
-
     public function delete(int $id): bool
     {
         $model = $this->findById($id);
@@ -103,6 +92,8 @@ class ProviderService
         return $model->delete();
     }
 
+    //
+
     public function getPending(int $page = 1, int $perPage = 10): LengthAwarePaginator
     {
         return Provider::query()
@@ -112,10 +103,13 @@ class ProviderService
             ->paginate($perPage, ['*'], 'page', $page);
     }
 
+    //
+
     public function approve(int $id): Provider
     {
         return DB::transaction(function () use ($id) {
             $provider = Provider::findOrFail($id);
+
             $provider->update(['approval_status' => ApprovalStatusEnum::ACCEPTED->value]);
 
             return $provider->fresh(['user', 'profileMedia']);
@@ -126,12 +120,15 @@ class ProviderService
     {
         return DB::transaction(function () use ($id) {
             $provider = Provider::findOrFail($id);
+
             $provider->update(['approval_status' => ApprovalStatusEnum::REJECTED->value]);
 
             return $provider->fresh(['user', 'profileMedia']);
         });
     }
 
+    //
+
     public function register(array $data): ?array
     {
         try {
@@ -173,6 +170,7 @@ class ProviderService
             $user->save();
 
             $provider = new Provider;
+
             $provider->user_id = $user->id;
             $provider->rg = $data['rg'] ?? null;
             $provider->document = $this->sanitizeDigits($data['document'] ?? null);
@@ -182,7 +180,9 @@ class ProviderService
             $provider->daily_price_4h = $data['daily_price_4h'] ?? null;
             $provider->daily_price_2h = $data['daily_price_2h'] ?? null;
             $provider->approval_status = ApprovalStatusEnum::PENDING->value;
+
             $provider->save();
+
             $provider->refresh();
 
             $selfie = $this->mediaService->createFromFile(
@@ -191,6 +191,7 @@ class ProviderService
                 source: 'provider',
                 sourceId: $provider->id,
             );
+
             $provider->profile_media_id = $selfie->id;
 
             $front = $this->mediaService->createFromFile(
@@ -200,6 +201,7 @@ class ProviderService
                 sourceId: $provider->id,
                 filename: 'frente.'.$data['document_front']->getClientOriginalExtension(),
             );
+
             $provider->document_front_media_id = $front->id;
 
             $back = $this->mediaService->createFromFile(
@@ -209,13 +211,15 @@ class ProviderService
                 sourceId: $provider->id,
                 filename: 'verso.'.$data['document_back']->getClientOriginalExtension(),
             );
+
             $provider->document_back_media_id = $back->id;
 
             $provider->save();
 
-            if (! empty($data['recipient_code'])) {
+            if (! empty($data['recipient_name']) && ! empty($data['recipient_default_bank_account'])) {
                 $this->pagarmeRecipientService->createRecipientForProvider($provider, $data);
             }
+
             $this->createProviderAddress($provider->id, $data);
             $this->createProviderServicesTypes($provider->id, $data);
             $this->createProviderWorkingDays($provider->id, $data);
@@ -234,13 +238,30 @@ class ProviderService
             return $result;
         } catch (\Exception $e) {
             DB::rollBack();
+
             Log::error('Error registering provider: '.$e->getMessage(), [
                 'data' => $data,
             ]);
+
             throw $e;
         }
     }
 
+    public function updateBankAccount(int $id, array $bankAccountData): ?Provider
+    {
+        $provider = $this->findById($id);
+
+        if (! $provider) {
+            return null;
+        }
+
+        $this->pagarmeRecipientService->updateDefaultBankAccount($provider, $bankAccountData);
+
+        return $provider->fresh(['user', 'profileMedia']);
+    }
+
+    //
+
     private function createProviderAddress(int $providerId, array $data): void
     {
         $state = null;
@@ -264,6 +285,7 @@ class ProviderService
         }
 
         $address = new Address;
+
         $address->source = 'provider';
         $address->source_id = $providerId;
         $address->zip_code = $this->sanitizeDigits($data['zip_code'] ?? null);
@@ -275,6 +297,7 @@ class ProviderService
         $address->address_type = $data['address_type'] ?? 'home';
         $address->state_id = $state?->id;
         $address->city_id = $city?->id;
+
         $address->save();
     }
 
@@ -295,10 +318,12 @@ class ProviderService
     private function createProviderWorkingDays(int $providerId, array $data): void
     {
         $workingDays = $data['working_days'] ?? [];
+
         $seen = [];
 
         foreach ($workingDays as $workingDay) {
             $day = (int) ($workingDay['day'] ?? -1);
+
             $period = $workingDay['period'] ?? null;
 
             if ($day < 0 || $day > 6 || ! in_array($period, ['morning', 'afternoon'], true)) {
@@ -306,6 +331,7 @@ class ProviderService
             }
 
             $uniqueKey = $day.'-'.$period;
+
             if (isset($seen[$uniqueKey])) {
                 continue;
             }

+ 1 - 1
app/Services/PushNotificationService.php

@@ -36,7 +36,7 @@ class PushNotificationService
             ->withNotification(Notification::create($notification->title(), $notification->body()))
             ->withAndroidConfig(AndroidConfig::fromArray([
                 'notification' => ['channel_id' => 'default'],
-                'priority' => 'high',
+                'priority'     => 'high',
             ]));
 
         $report = $this->messaging->sendMulticast($message, $tokens);

+ 46 - 0
database/migrations/2026_06_03_163406_add_idempotency_key_to_related_tables.php

@@ -0,0 +1,46 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::table('providers', function (Blueprint $table) {
+            $table->string('idempotency_key')->nullable()->after('recipient_code');
+        });
+
+        Schema::table('payments', function (Blueprint $table) {
+            $table->string('idempotency_key')->nullable()->after('gateway_code');
+        });
+
+        Schema::table('clients', function (Blueprint $table) {
+            $table->string('idempotency_key')->nullable()->after('external_customer_code');
+        });
+
+        Schema::table('client_payment_methods', function (Blueprint $table) {
+            $table->string('idempotency_key')->nullable()->after('gateway_card_id');
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::table('providers', function (Blueprint $table) {
+            $table->dropColumn('idempotency_key');
+        });
+
+        Schema::table('payments', function (Blueprint $table) {
+            $table->dropColumn('idempotency_key');
+        });
+
+        Schema::table('clients', function (Blueprint $table) {
+            $table->dropColumn('idempotency_key');
+        });
+
+        Schema::table('client_payment_methods', function (Blueprint $table) {
+            $table->dropColumn('idempotency_key');
+        });
+    }
+};

+ 24 - 0
database/migrations/2026_06_03_163929_rename_external_customer_columns_to_gateway_prefix_on_clients_table.php

@@ -0,0 +1,24 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::table('clients', function (Blueprint $table) {
+            $table->renameColumn('external_customer_id', 'gateway_customer_id');
+            $table->renameColumn('external_customer_code', 'gateway_customer_code');
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::table('clients', function (Blueprint $table) {
+            $table->renameColumn('gateway_customer_id', 'external_customer_id');
+            $table->renameColumn('gateway_customer_code', 'external_customer_code');
+        });
+    }
+};

+ 3 - 2
pint.json

@@ -3,10 +3,11 @@
     "exclude": [
         "database/migrations",
         "routes",
-        "app/Models",
+        "app/Data/Pagarme",
         "app/Enums",
         "app/Http/Requests",
-        "app/Data/Pagarme"
+        "app/Models",
+        "app/Services/Pagarme"
     ],
     "rules": {
         "binary_operator_spaces": {

+ 5 - 2
routes/authRoutes/payment.php

@@ -3,9 +3,12 @@
 use Illuminate\Support\Facades\Route;
 use App\Http\Controllers\PaymentController;
 
-Route::get('/payment',         [PaymentController::class, 'index'])->middleware('permission:payment,view');
-Route::post('/payment',        [PaymentController::class, 'store'])->middleware('permission:payment,add');
+Route::get('/payment',  [PaymentController::class, 'index'])->middleware('permission:payment,view');
+Route::post('/payment', [PaymentController::class, 'store'])->middleware('permission:payment,add');
+
+Route::get('/payment/schedule/{schedule}/pix',  [PaymentController::class, 'getSchedulePix'])->middleware('permission:config.schedule,view');
 Route::post('/payment/schedule/{schedule}/pay', [PaymentController::class, 'paySchedule'])->middleware('permission:config.schedule,edit');
+
 Route::get('/payment/{id}',    [PaymentController::class, 'show'])->middleware('permission:payment,view');
 Route::put('/payment/{id}',    [PaymentController::class, 'update'])->middleware('permission:payment,edit');
 Route::delete('/payment/{id}', [PaymentController::class, 'destroy'])->middleware('permission:payment,delete');

+ 34 - 2
routes/console.php

@@ -1,10 +1,13 @@
 <?php
 
-use Illuminate\Foundation\Inspiring;
-use Illuminate\Support\Facades\Artisan;
 use App\Commands\CreateCrud;
 use App\Commands\RefreshPermissions;
 use App\Commands\TestWebsocketEvent;
+use App\Models\Provider;
+use App\Services\Pagarme\PagarmeTransferService;
+use App\Services\ProviderWithdrawalService;
+use Illuminate\Foundation\Inspiring;
+use Illuminate\Support\Facades\Artisan;
 
 Artisan::command('inspire', function () {
     $this->comment(Inspiring::quote());
@@ -35,3 +38,32 @@ Artisan::command('websocket:test {room} {--event=test-event} {--data=}', functio
         '--data' => $this->option('data'),
     ]);
 })->purpose('Test websocket broadcasting by emitting an event');
+
+//
+
+Artisan::command('pagarme:recipient-balance {recipient_id} {--skip-local}', function (
+    PagarmeTransferService $pagarmeTransfer,
+    ProviderWithdrawalService $withdrawals,
+) {
+    $recipientId = $this->argument('recipient_id');
+
+    if (! $this->option('skip-local')) {
+        $provider = Provider::query()
+            ->where('recipient_id', $recipientId)
+            ->first();
+
+        if ($provider) {
+            $this->info('Saldo calculado localmente:');
+            $this->line(sprintf('  available: R$ %s', number_format($withdrawals->getAvailableBalance($provider), 2, ',', '.')));
+            $this->line(sprintf('  pending:   R$ %s', number_format($withdrawals->getPendingBalance($provider), 2, ',', '.')));
+            $this->newLine();
+        } else {
+            $this->warn('Nenhum provider local encontrado para esse recipient_id.');
+            $this->newLine();
+        }
+    }
+
+    $this->info('Saldo retornado pelo Pagar.me:');
+
+    $this->line(json_encode($pagarmeTransfer->getRecipientBalance($recipientId), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
+})->purpose('Consult Pagar.me recipient balance and compare it with local withdrawal balance');