Bläddra i källkod

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

Gustavo Zanatta 4 dagar sedan
förälder
incheckning
c9da6dd3bd
100 ändrade filer med 2424 tillägg och 928 borttagningar
  1. 14 0
      .env.example
  2. 44 0
      _ide_helper.php
  3. 35 0
      app/Data/Pagarme/Anticipation/AnticipationLimitData.php
  4. 29 0
      app/Data/Pagarme/Anticipation/BulkAnticipationLimitsResponseData.php
  5. 29 0
      app/Data/Pagarme/Anticipation/BulkAnticipationRequestData.php
  6. 71 0
      app/Data/Pagarme/Anticipation/BulkAnticipationResponseData.php
  7. 3 2
      app/Data/Pagarme/Card/CardRequestData.php
  8. 1 1
      app/Data/Pagarme/Card/CardResponseData.php
  9. 2 2
      app/Data/Pagarme/Card/Parts/Request/BillingAddressData.php
  10. 6 5
      app/Data/Pagarme/Customer/CustomerRequestData.php
  11. 6 5
      app/Data/Pagarme/Customer/CustomerResponseData.php
  12. 2 2
      app/Data/Pagarme/Customer/Parts/Request/AddressData.php
  13. 2 2
      app/Data/Pagarme/Customer/Parts/Request/PhoneData.php
  14. 3 3
      app/Data/Pagarme/Customer/Parts/Request/PhonesData.php
  15. 2 2
      app/Data/Pagarme/Customer/Parts/Response/AddressData.php
  16. 2 2
      app/Data/Pagarme/Customer/Parts/Response/PhoneData.php
  17. 6 6
      app/Data/Pagarme/Customer/Parts/Response/PhonesData.php
  18. 19 22
      app/Data/Pagarme/Order/OrderRequestData.php
  19. 17 15
      app/Data/Pagarme/Order/OrderResponseData.php
  20. 2 2
      app/Data/Pagarme/Order/Parts/Request/CreditCardData.php
  21. 2 2
      app/Data/Pagarme/Order/Parts/Request/ItemData.php
  22. 9 14
      app/Data/Pagarme/Order/Parts/Request/PaymentData.php
  23. 2 2
      app/Data/Pagarme/Order/Parts/Request/PixAdditionalInformationData.php
  24. 3 3
      app/Data/Pagarme/Order/Parts/Request/PixData.php
  25. 3 3
      app/Data/Pagarme/Order/Parts/Request/SplitData.php
  26. 2 2
      app/Data/Pagarme/Order/Parts/Request/SplitOptionsData.php
  27. 5 5
      app/Data/Pagarme/Order/Parts/Response/ChargeData.php
  28. 2 2
      app/Data/Pagarme/Order/Parts/Response/CheckoutData.php
  29. 2 2
      app/Data/Pagarme/Order/Parts/Response/ItemData.php
  30. 2 2
      app/Data/Pagarme/Order/Parts/Response/TransactionData.php
  31. 10 10
      app/Data/Pagarme/Recipient/BankAccountUpdateRequestData.php
  32. 2 2
      app/Data/Pagarme/Recipient/Parts/Request/AddressData.php
  33. 39 0
      app/Data/Pagarme/Recipient/Parts/Request/AutomaticAnticipationSettingsData.php
  34. 11 11
      app/Data/Pagarme/Recipient/Parts/Request/BankAccountData.php
  35. 2 2
      app/Data/Pagarme/Recipient/Parts/Request/PhoneData.php
  36. 17 0
      app/Data/Pagarme/Recipient/Parts/Request/PhoneNumbersData.php
  37. 4 5
      app/Data/Pagarme/Recipient/Parts/Request/RegisterInformationData.php
  38. 2 2
      app/Data/Pagarme/Recipient/Parts/Request/TransferSettingsData.php
  39. 2 2
      app/Data/Pagarme/Recipient/Parts/Response/BankAccountData.php
  40. 9 6
      app/Data/Pagarme/Recipient/RecipientRequestData.php
  41. 4 3
      app/Data/Pagarme/Recipient/RecipientResponseData.php
  42. 0 19
      app/Data/Pagarme/Request/RecipientRequestData/RecipientAutomaticAnticipationSettingsData.php
  43. 0 17
      app/Data/Pagarme/Request/RecipientRequestData/RecipientRegisterInformationData/RecipientPhoneNumbersData/RecipientPhoneNumbersData.php
  44. 1 1
      app/Data/Pagarme/Transfer/TransferRequestData.php
  45. 1 1
      app/Data/Pagarme/Transfer/TransferResponseData.php
  46. 14 0
      app/Enums/CartStatusEnum.php
  47. 2 2
      app/Http/Controllers/AddressController.php
  48. 82 0
      app/Http/Controllers/CartController.php
  49. 50 0
      app/Http/Controllers/CartItemController.php
  50. 3 3
      app/Http/Controllers/CityController.php
  51. 2 2
      app/Http/Controllers/ClientController.php
  52. 2 2
      app/Http/Controllers/ClientFavoriteProviderController.php
  53. 2 2
      app/Http/Controllers/ClientPaymentMethodController.php
  54. 2 2
      app/Http/Controllers/ClientProviderBlockController.php
  55. 2 2
      app/Http/Controllers/CountryController.php
  56. 2 2
      app/Http/Controllers/MediaController.php
  57. 57 24
      app/Http/Controllers/PaymentController.php
  58. 2 2
      app/Http/Controllers/PermissionController.php
  59. 2 2
      app/Http/Controllers/ProviderBlockedDayController.php
  60. 2 2
      app/Http/Controllers/ProviderClientBlockController.php
  61. 2 2
      app/Http/Controllers/ProviderController.php
  62. 5 0
      app/Http/Controllers/ProviderWithdrawalController.php
  63. 1 1
      app/Http/Controllers/ProviderWorkingDayController.php
  64. 2 2
      app/Http/Controllers/ReviewController.php
  65. 2 2
      app/Http/Controllers/ReviewImprovementController.php
  66. 47 22
      app/Http/Controllers/ScheduleController.php
  67. 2 2
      app/Http/Controllers/StateController.php
  68. 2 2
      app/Http/Controllers/WebhookController.php
  69. 33 0
      app/Http/Requests/CartItemRequest.php
  70. 81 0
      app/Http/Requests/CartRequest.php
  71. 10 36
      app/Http/Requests/ScheduleRequest.php
  72. 37 0
      app/Http/Resources/CartItemResource.php
  73. 43 0
      app/Http/Resources/CartResource.php
  74. 6 4
      app/Http/Resources/PaymentResource.php
  75. 25 14
      app/Http/Resources/ScheduleResource.php
  76. 35 12
      app/Jobs/FinishScheduleJob.php
  77. 2 0
      app/Jobs/StartScheduleJob.php
  78. 26 26
      app/Models/Address.php
  79. 58 0
      app/Models/Cart.php
  80. 41 0
      app/Models/CartItem.php
  81. 2 2
      app/Models/Client.php
  82. 38 33
      app/Rules/ScheduleBusinessRules.php
  83. 52 0
      app/Services/CartItemService.php
  84. 149 0
      app/Services/CartService.php
  85. 137 66
      app/Services/DashboardService.php
  86. 26 0
      app/Services/Pagarme/Concerns/FormatsPagarmeData.php
  87. 4 5
      app/Services/Pagarme/Concerns/SendsPagarmeRequests.php
  88. 202 0
      app/Services/Pagarme/PagarmeAnticipationService.php
  89. 4 4
      app/Services/Pagarme/PagarmeCardService.php
  90. 16 17
      app/Services/Pagarme/PagarmeCustomerService.php
  91. 184 34
      app/Services/Pagarme/PagarmePaymentService.php
  92. 26 22
      app/Services/Pagarme/PagarmeRecipientService.php
  93. 2 2
      app/Services/Pagarme/PagarmeTransferService.php
  94. 175 27
      app/Services/PaymentService.php
  95. 39 23
      app/Services/ProviderWithdrawalService.php
  96. 217 243
      app/Services/ScheduleService.php
  97. 23 13
      app/Services/WebhookService.php
  98. 0 1
      composer.json
  99. 4 73
      composer.lock
  100. 7 2
      config/services.php

+ 14 - 0
.env.example

@@ -39,6 +39,8 @@ BROADCAST_DRIVER=log
 
 SANCTUM_STATEFUL_DOMAINS=localhost
 
+#
+
 PAGARME_BASE_URL=https://api.pagar.me/core/v5
 
 PAGARME_SECRET_KEY=
@@ -49,4 +51,16 @@ PAGARME_WEBHOOK_PASSWORD=
 
 PAGARME_PLATFORM_RECIPIENT_ID=
 
+#
+
 PAGARME_PIX_DISABLE_SPLIT=false
+
+PAGARME_PLATFORM_PIX_FEE_RATE=0.12
+
+PAGARME_PLATFORM_CREDIT_CARD_FEE_RATE=0.16
+
+PAGARME_PLATFORM_CART_MIN_3_SCHEDULES_FEE_RATE=0.10
+
+PAGARME_TRANSFER_FEE_AMOUNT=3.67
+
+PAGARME_WITHDRAWAL_RELEASE_DAYS=2

+ 44 - 0
_ide_helper.php

@@ -22782,6 +22782,49 @@ 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 {
     /**
      */
@@ -27678,6 +27721,7 @@ 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 {}
 }
 
 

+ 35 - 0
app/Data/Pagarme/Anticipation/AnticipationLimitData.php

@@ -0,0 +1,35 @@
+<?php
+
+namespace App\Data\Pagarme\Anticipation;
+
+use App\Data\Pagarme\PagarmeResponseData;
+
+final readonly class AnticipationLimitData extends PagarmeResponseData
+{
+    public function __construct(
+        public int $amount,
+        public int $anticipationFee,
+        public int $fee,
+        public int $fraudCoverageFee,
+    ) {}
+
+    public static function fromArray(array $payload): static
+    {
+        return new self(
+            amount:           static::arrInt($payload, 'amount')             ?? 0,
+            anticipationFee:  static::arrInt($payload, 'anticipation_fee')   ?? 0,
+            fee:              static::arrInt($payload, 'fee')                ?? 0,
+            fraudCoverageFee: static::arrInt($payload, 'fraud_coverage_fee') ?? 0,
+        );
+    }
+
+    public function toArray(): array
+    {
+        return [
+            'amount'              => $this->amount,
+            'anticipation_fee'    => $this->anticipationFee,
+            'fee'                 => $this->fee,
+            'fraud_coverage_fee'  => $this->fraudCoverageFee,
+        ];
+    }
+}

+ 29 - 0
app/Data/Pagarme/Anticipation/BulkAnticipationLimitsResponseData.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace App\Data\Pagarme\Anticipation;
+
+use App\Data\Pagarme\PagarmeResponseData;
+
+final readonly class BulkAnticipationLimitsResponseData extends PagarmeResponseData
+{
+    public function __construct(
+        public AnticipationLimitData $maximum,
+        public AnticipationLimitData $minimum,
+    ) {}
+
+    public static function fromArray(array $payload): static
+    {
+        return new self(
+            maximum: AnticipationLimitData::fromArray(static::arrArray($payload, 'maximum')),
+            minimum: AnticipationLimitData::fromArray(static::arrArray($payload, 'minimum')),
+        );
+    }
+
+    public function toArray(): array
+    {
+        return [
+            'maximum' => $this->maximum->toArray(),
+            'minimum' => $this->minimum->toArray(),
+        ];
+    }
+}

+ 29 - 0
app/Data/Pagarme/Anticipation/BulkAnticipationRequestData.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace App\Data\Pagarme\Anticipation;
+
+use App\Data\Pagarme\PagarmeData;
+
+final readonly class BulkAnticipationRequestData extends PagarmeData
+{
+    public function __construct(
+        public string $paymentDate,
+        public string $timeframe,
+        public int    $requestedAmount,
+        public bool   $automaticTransfer = false,
+    ) {
+        self::requireFilled($this->paymentDate, 'payment_date');
+        self::requireIn($this->timeframe, ['start', 'end'], 'timeframe');
+        self::requirePositiveInt($this->requestedAmount, 'requested_amount');
+    }
+
+    public function toArray(): array
+    {
+        return $this->filterFilledRecursive([
+            'payment_date'       => $this->paymentDate,
+            'timeframe'          => $this->timeframe,
+            'requested_amount'   => $this->requestedAmount,
+            'automatic_transfer' => $this->automaticTransfer,
+        ]);
+    }
+}

+ 71 - 0
app/Data/Pagarme/Anticipation/BulkAnticipationResponseData.php

@@ -0,0 +1,71 @@
+<?php
+
+namespace App\Data\Pagarme\Anticipation;
+
+use App\Data\Pagarme\PagarmeResponseData;
+
+final readonly class BulkAnticipationResponseData extends PagarmeResponseData
+{
+    public function __construct(
+        public ?string $id,
+        public ?string $status,
+        public ?int    $amount,
+        public ?int    $fee,
+        public ?int    $fraudCoverageFee,
+        public ?int    $anticipationFee,
+        public ?bool   $automaticTransfer,
+        public ?string $type,
+        public ?string $timeframe,
+        public ?string $paymentDate,
+        public ?string $createdAt       = null,
+        public ?string $updatedAt       = null,
+        public mixed   $anticipationTax = null,
+    ) {}
+
+    public function requireId(): string
+    {
+        if (! $this->id) {
+            throw new \RuntimeException('Pagar.me bulk anticipation creation returned an empty id.');
+        }
+
+        return $this->id;
+    }
+
+    public static function fromArray(array $payload): static
+    {
+        return new self(
+            id:                static::arrString($payload, 'id'),
+            status:            static::arrString($payload, 'status'),
+            amount:            static::arrInt($payload, 'amount'),
+            fee:               static::arrInt($payload, 'fee'),
+            fraudCoverageFee:  static::arrInt($payload, 'fraud_coverage_fee'),
+            anticipationFee:   static::arrInt($payload, 'anticipation_fee'),
+            automaticTransfer: static::arrBool($payload, 'automatic_transfer'),
+            type:              static::arrString($payload, 'type'),
+            timeframe:         static::arrString($payload, 'timeframe'),
+            paymentDate:       static::arrString($payload, 'payment_date'),
+            createdAt:         static::arrString($payload, 'created_at'),
+            updatedAt:         static::arrString($payload, 'updated_at'),
+            anticipationTax:   static::arrGet($payload, 'anticipation_tax'),
+        );
+    }
+
+    public function toArray(): array
+    {
+        return array_filter([
+            'id'                 => $this->id,
+            'status'             => $this->status,
+            'amount'             => $this->amount,
+            'fee'                => $this->fee,
+            'fraud_coverage_fee' => $this->fraudCoverageFee,
+            'anticipation_fee'   => $this->anticipationFee,
+            'automatic_transfer' => $this->automaticTransfer,
+            'type'               => $this->type,
+            'timeframe'          => $this->timeframe,
+            'payment_date'       => $this->paymentDate,
+            'created_at'         => $this->createdAt,
+            'updated_at'         => $this->updatedAt,
+            'anticipation_tax'   => $this->anticipationTax,
+        ], static fn ($v) => $v !== null);
+    }
+}

+ 3 - 2
app/Data/Pagarme/Request/CardRequestData/CardRequestData.php → app/Data/Pagarme/Card/CardRequestData.php

@@ -1,8 +1,9 @@
 <?php
 
-namespace App\Data\Pagarme\Request\CardRequestData;
+namespace App\Data\Pagarme\Card;
 
 use App\Data\Pagarme\PagarmeData;
+use App\Data\Pagarme\Card\Parts\Request\BillingAddressData;
 
 final readonly class CardRequestData extends PagarmeData
 {
@@ -10,7 +11,7 @@ final readonly class CardRequestData extends PagarmeData
         public string  $token,
         public ?string $label = null,
 
-        public ?CardBillingAddressData $billingAddress = null,
+        public ?BillingAddressData $billingAddress = null,
     ) {
         self::requireFilled($this->token, 'token');
     }

+ 1 - 1
app/Data/Pagarme/Response/CardResponseData.php → app/Data/Pagarme/Card/CardResponseData.php

@@ -1,6 +1,6 @@
 <?php
 
-namespace App\Data\Pagarme\Response;
+namespace App\Data\Pagarme\Card;
 
 use App\Data\Pagarme\PagarmeResponseData;
 

+ 2 - 2
app/Data/Pagarme/Request/CardRequestData/CardBillingAddressData.php → app/Data/Pagarme/Card/Parts/Request/BillingAddressData.php

@@ -1,11 +1,11 @@
 <?php
 
-namespace App\Data\Pagarme\Request\CardRequestData;
+namespace App\Data\Pagarme\Card\Parts\Request;
 
 use App\Data\Pagarme\PagarmeData;
 use App\Models\Address;
 
-final readonly class CardBillingAddressData extends PagarmeData
+final readonly class BillingAddressData extends PagarmeData
 {
     public function __construct(
         public string  $line1,

+ 6 - 5
app/Data/Pagarme/Request/CustomerRequestData/CustomerRequestData.php → app/Data/Pagarme/Customer/CustomerRequestData.php

@@ -1,9 +1,10 @@
 <?php
 
-namespace App\Data\Pagarme\Request\CustomerRequestData;
+namespace App\Data\Pagarme\Customer;
 
 use App\Data\Pagarme\PagarmeData;
-use App\Data\Pagarme\Request\CustomerRequestData\CustomerPhonesRequestData\CustomerPhonesRequestData;
+use App\Data\Pagarme\Customer\Parts\Request\AddressData;
+use App\Data\Pagarme\Customer\Parts\Request\PhonesData;
 
 final readonly class CustomerRequestData extends PagarmeData
 {
@@ -15,14 +16,14 @@ final readonly class CustomerRequestData extends PagarmeData
         public string $documentType,
         public string $code,
 
-        public ?CustomerAddressRequestData $address = null,
-        public ?CustomerPhonesRequestData  $phones  = null,
+        public ?AddressData $address = null,
+        public ?PhonesData  $phones  = null,
     ) {
         self::requireFilled($this->name, 'name');
         self::requireFilled($this->email, 'email');
         self::requireFilled($this->document, 'document');
         self::requireIn($this->type, ['individual', 'company'], 'type');
-        self::requireIn($this->documentType, ['CPF', 'CNPJ'], 'document_type');
+        self::requireIn($this->documentType, ['CPF', 'CNPJ', 'PASSPORT'], 'document_type');
         self::requireFilled($this->code, 'code');
     }
 

+ 6 - 5
app/Data/Pagarme/Response/CustomerResponseData/CustomerResponseData.php → app/Data/Pagarme/Customer/CustomerResponseData.php

@@ -1,8 +1,9 @@
 <?php
 
-namespace App\Data\Pagarme\Response\CustomerResponseData;
+namespace App\Data\Pagarme\Customer;
 
-use App\Data\Pagarme\Response\CustomerResponseData\CustomerPhonesResponseData\CustomerPhonesResponseData;
+use App\Data\Pagarme\Customer\Parts\Response\AddressData;
+use App\Data\Pagarme\Customer\Parts\Response\PhonesData;
 use App\Data\Pagarme\PagarmeResponseData;
 
 final readonly class CustomerResponseData extends PagarmeResponseData
@@ -17,7 +18,7 @@ final readonly class CustomerResponseData extends PagarmeResponseData
         public ?string $type,
         public ?bool   $delinquent,
 
-        public ?CustomerAddressResponseData $address, public CustomerPhonesResponseData $phones,
+        public ?AddressData $address, public PhonesData $phones,
 
         public ?string $createdAt = null,
         public ?string $updatedAt = null,
@@ -49,9 +50,9 @@ final readonly class CustomerResponseData extends PagarmeResponseData
             type:         static::arrString($payload, 'type'),
             delinquent:   static::arrBool($payload, 'delinquent'),
 
-            address: ! empty($address) ? CustomerAddressResponseData::fromArray($address) : null,
+            address: ! empty($address) ? AddressData::fromArray($address) : null,
 
-            phones:    CustomerPhonesResponseData::fromArray($phones),
+            phones:    PhonesData::fromArray($phones),
             createdAt: static::arrString($payload, 'created_at'),
             updatedAt: static::arrString($payload, 'updated_at'),
         );

+ 2 - 2
app/Data/Pagarme/Request/CustomerRequestData/CustomerAddressRequestData.php → app/Data/Pagarme/Customer/Parts/Request/AddressData.php

@@ -1,10 +1,10 @@
 <?php
 
-namespace App\Data\Pagarme\Request\CustomerRequestData;
+namespace App\Data\Pagarme\Customer\Parts\Request;
 
 use App\Data\Pagarme\PagarmeData;
 
-final readonly class CustomerAddressRequestData extends PagarmeData
+final readonly class AddressData extends PagarmeData
 {
     public function __construct(
         public ?string $line1,

+ 2 - 2
app/Data/Pagarme/Request/CustomerRequestData/CustomerPhonesRequestData/CustomerPhoneData.php → app/Data/Pagarme/Customer/Parts/Request/PhoneData.php

@@ -1,10 +1,10 @@
 <?php
 
-namespace App\Data\Pagarme\Request\CustomerRequestData\CustomerPhonesRequestData;
+namespace App\Data\Pagarme\Customer\Parts\Request;
 
 use App\Data\Pagarme\PagarmeData;
 
-final readonly class CustomerPhoneData extends PagarmeData
+final readonly class PhoneData extends PagarmeData
 {
     public function __construct(
         public string $countryCode,

+ 3 - 3
app/Data/Pagarme/Request/CustomerRequestData/CustomerPhonesRequestData/CustomerPhonesRequestData.php → app/Data/Pagarme/Customer/Parts/Request/PhonesData.php

@@ -1,13 +1,13 @@
 <?php
 
-namespace App\Data\Pagarme\Request\CustomerRequestData\CustomerPhonesRequestData;
+namespace App\Data\Pagarme\Customer\Parts\Request;
 
 use App\Data\Pagarme\PagarmeData;
 
-final readonly class CustomerPhonesRequestData extends PagarmeData
+final readonly class PhonesData extends PagarmeData
 {
     public function __construct(
-        public ?CustomerPhoneData $mobilePhone = null,
+        public ?PhoneData $mobilePhone = null,
     ) {}
 
     public function toArray(): array

+ 2 - 2
app/Data/Pagarme/Response/CustomerResponseData/CustomerAddressResponseData.php → app/Data/Pagarme/Customer/Parts/Response/AddressData.php

@@ -1,10 +1,10 @@
 <?php
 
-namespace App\Data\Pagarme\Response\CustomerResponseData;
+namespace App\Data\Pagarme\Customer\Parts\Response;
 
 use App\Data\Pagarme\PagarmeResponseData;
 
-final readonly class CustomerAddressResponseData extends PagarmeResponseData
+final readonly class AddressData extends PagarmeResponseData
 {
     public function __construct(
         public ?string $id,

+ 2 - 2
app/Data/Pagarme/Response/CustomerResponseData/CustomerPhonesResponseData/PhoneResponseData.php → app/Data/Pagarme/Customer/Parts/Response/PhoneData.php

@@ -1,10 +1,10 @@
 <?php
 
-namespace App\Data\Pagarme\Response\CustomerResponseData\CustomerPhonesResponseData;
+namespace App\Data\Pagarme\Customer\Parts\Response;
 
 use App\Data\Pagarme\PagarmeResponseData;
 
-final readonly class PhoneResponseData extends PagarmeResponseData
+final readonly class PhoneData extends PagarmeResponseData
 {
     public function __construct(
         public ?string $countryCode,

+ 6 - 6
app/Data/Pagarme/Response/CustomerResponseData/CustomerPhonesResponseData/CustomerPhonesResponseData.php → app/Data/Pagarme/Customer/Parts/Response/PhonesData.php

@@ -1,14 +1,14 @@
 <?php
 
-namespace App\Data\Pagarme\Response\CustomerResponseData\CustomerPhonesResponseData;
+namespace App\Data\Pagarme\Customer\Parts\Response;
 
 use App\Data\Pagarme\PagarmeResponseData;
 
-final readonly class CustomerPhonesResponseData extends PagarmeResponseData
+final readonly class PhonesData extends PagarmeResponseData
 {
     public function __construct(
-        public ?PhoneResponseData $homePhone,
-        public ?PhoneResponseData $mobilePhone,
+        public ?PhoneData $homePhone,
+        public ?PhoneData $mobilePhone,
     ) {}
 
     public static function fromArray(array $payload): static
@@ -17,8 +17,8 @@ final readonly class CustomerPhonesResponseData extends PagarmeResponseData
         $mobile = static::arrArray($payload, 'mobile_phone');
 
         return new self(
-            homePhone:   ! empty($home)   ? PhoneResponseData::fromArray($home)   : null,
-            mobilePhone: ! empty($mobile) ? PhoneResponseData::fromArray($mobile) : null,
+            homePhone:   ! empty($home)   ? PhoneData::fromArray($home)   : null,
+            mobilePhone: ! empty($mobile) ? PhoneData::fromArray($mobile) : null,
         );
     }
 

+ 19 - 22
app/Data/Pagarme/Request/OrderRequestData/OrderRequestData.php → app/Data/Pagarme/Order/OrderRequestData.php

@@ -1,24 +1,24 @@
 <?php
 
-namespace App\Data\Pagarme\Request\OrderRequestData;
+namespace App\Data\Pagarme\Order;
 
+use App\Data\Pagarme\Customer\CustomerRequestData as CustomerData;
 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\OrderPixData;
-use App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData\OrderSplitData\OrderSplitData;
-use App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData\OrderSplitData\OrderSplitOptionsData;
+use App\Data\Pagarme\Order\Parts\Request\CreditCardData;
+use App\Data\Pagarme\Order\Parts\Request\ItemData;
+use App\Data\Pagarme\Order\Parts\Request\PaymentData;
+use App\Data\Pagarme\Order\Parts\Request\PixData;
+use App\Data\Pagarme\Order\Parts\Request\SplitData;
+use App\Data\Pagarme\Order\Parts\Request\SplitOptionsData;
 use App\Models\PaymentSplit;
 use Illuminate\Support\Collection;
 
 final readonly class OrderRequestData extends PagarmeData
 {
     /**
-     * @param  OrderItemData[]  $items
-     * @param  OrderPaymentData[]  $payments
+     * @param  ItemData[]  $items
+     * @param  PaymentData[]  $payments
      */
-
     public function __construct(
         public string  $code,
         public array   $items,
@@ -53,39 +53,36 @@ final readonly class OrderRequestData extends PagarmeData
     }
 
     /**
-     * @param  OrderSplitData[]|null  $split
+     * @param  SplitData[]|null  $split
      */
-
-    public static function creditCardPaymentMethod(OrderCreditCardData $creditCard, ?array $split = null): OrderPaymentData
+    public static function creditCardPaymentMethod(CreditCardData $creditCard, ?array $split = null): PaymentData
     {
-        return OrderPaymentData::creditCard($creditCard, $split);
+        return PaymentData::creditCard($creditCard, $split);
     }
 
     /**
-     * @param  OrderSplitData[]|null  $split
+     * @param  SplitData[]|null  $split
      */
-
-    public static function pixPaymentMethod(OrderPixData $pix, ?array $split = null): OrderPaymentData
+    public static function pixPaymentMethod(PixData $pix, ?array $split = null): PaymentData
     {
-        return OrderPaymentData::pix($pix, $split);
+        return PaymentData::pix($pix, $split);
     }
 
     /**
      * @param  Collection<PaymentSplit>  $transfers
-     * @return OrderSplitData[]
+     * @return SplitData[]
      */
-
     public static function splitFromTransfers(Collection $transfers): array
     {
         return $transfers
             ->filter(fn (PaymentSplit $split) => ! empty($split->gateway_transfer_target_reference))
             ->map(function (PaymentSplit $split) {
-                return new OrderSplitData(
+                return new SplitData(
                     amount:      self::amountInCents((float) $split->gross_amount),
                     recipientId: $split->gateway_transfer_target_reference,
                     type:        'flat',
 
-                    options: new OrderSplitOptionsData(
+                    options: new SplitOptionsData(
                         chargeProcessingFee: false,
                         chargeRemainderFee:  false,
                         liable:              false,

+ 17 - 15
app/Data/Pagarme/Response/OrderResponseData/OrderResponseData.php → app/Data/Pagarme/Order/OrderResponseData.php

@@ -1,17 +1,19 @@
 <?php
 
-namespace App\Data\Pagarme\Response\OrderResponseData;
+namespace App\Data\Pagarme\Order;
 
-use App\Data\Pagarme\Response\CustomerResponseData\CustomerResponseData;
-use App\Data\Pagarme\Response\OrderResponseData\OrderChargeResponseData\OrderChargeResponseData;
-use App\Data\Pagarme\Response\OrderResponseData\OrderChargeResponseData\OrderTransactionResponseData;
+use App\Data\Pagarme\Customer\CustomerResponseData;
+use App\Data\Pagarme\Order\Parts\Response\ChargeData;
+use App\Data\Pagarme\Order\Parts\Response\CheckoutData;
+use App\Data\Pagarme\Order\Parts\Response\ItemData;
+use App\Data\Pagarme\Order\Parts\Response\TransactionData;
 use App\Data\Pagarme\PagarmeResponseData;
 use App\Enums\PaymentStatusEnum;
 
 /**
- * @param  OrderItemResponseData[]  $items
- * @param  OrderChargeResponseData[]  $charges
- * @param  OrderCheckoutResponseData[]  $checkouts
+ * @param  ItemData[]  $items
+ * @param  ChargeData[]  $charges
+ * @param  CheckoutData[]  $checkouts
  */
 final readonly class OrderResponseData extends PagarmeResponseData
 {
@@ -98,7 +100,7 @@ final readonly class OrderResponseData extends PagarmeResponseData
         return $message;
     }
 
-    public function firstCharge(): ?OrderChargeResponseData
+    public function firstCharge(): ?ChargeData
     {
         return $this->charges[0] ?? null;
     }
@@ -133,7 +135,7 @@ final readonly class OrderResponseData extends PagarmeResponseData
         return $transaction?->id ?? $charge?->id ?? $this->id;
     }
 
-    public function lastTransaction(): ?OrderTransactionResponseData
+    public function lastTransaction(): ?TransactionData
     {
         return $this->firstCharge()?->transaction();
     }
@@ -189,7 +191,7 @@ final readonly class OrderResponseData extends PagarmeResponseData
             status:   static::arrString($payload, 'status'),
 
             items: static::arrMap($payload, 'items',
-                static fn (array $item) => OrderItemResponseData::fromArray($item),
+                static fn (array $item) => ItemData::fromArray($item),
             ),
 
             customer: ! empty($customer)
@@ -197,11 +199,11 @@ final readonly class OrderResponseData extends PagarmeResponseData
                 : null,
 
             charges: static::arrMap($payload, 'charges',
-                static fn (array $charge) => OrderChargeResponseData::fromArray($charge),
+                static fn (array $charge) => ChargeData::fromArray($charge),
             ),
 
             checkouts: static::arrMap($payload, 'checkouts',
-                static fn (array $checkout) => OrderCheckoutResponseData::fromArray($checkout),
+                static fn (array $checkout) => CheckoutData::fromArray($checkout),
             ),
 
             metadata:  static::arrArray($payload, 'metadata'),
@@ -221,7 +223,7 @@ final readonly class OrderResponseData extends PagarmeResponseData
             'closed'   => $this->closed,
 
             'items' => array_map(
-                static fn (OrderItemResponseData $item) => $item->toArray(),
+                static fn (ItemData $item) => $item->toArray(),
                 $this->items,
             ),
 
@@ -232,12 +234,12 @@ final readonly class OrderResponseData extends PagarmeResponseData
             'closed_at'  => $this->closedAt,
 
             'charges' => array_map(
-                static fn (OrderChargeResponseData $charge) => $charge->toArray(),
+                static fn (ChargeData $charge) => $charge->toArray(),
                 $this->charges,
             ),
 
             'checkouts' => array_map(
-                static fn (OrderCheckoutResponseData $checkout) => $checkout->toArray(),
+                static fn (CheckoutData $checkout) => $checkout->toArray(),
                 $this->checkouts,
             ),
 

+ 2 - 2
app/Data/Pagarme/Request/OrderRequestData/OrderPaymentData/OrderCreditCardData.php → app/Data/Pagarme/Order/Parts/Request/CreditCardData.php

@@ -1,10 +1,10 @@
 <?php
 
-namespace App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData;
+namespace App\Data\Pagarme\Order\Parts\Request;
 
 use App\Data\Pagarme\PagarmeData;
 
-final readonly class OrderCreditCardData extends PagarmeData
+final readonly class CreditCardData extends PagarmeData
 {
     public function __construct(
         public string  $cardId,

+ 2 - 2
app/Data/Pagarme/Request/OrderRequestData/OrderItemData.php → app/Data/Pagarme/Order/Parts/Request/ItemData.php

@@ -1,10 +1,10 @@
 <?php
 
-namespace App\Data\Pagarme\Request\OrderRequestData;
+namespace App\Data\Pagarme\Order\Parts\Request;
 
 use App\Data\Pagarme\PagarmeData;
 
-final readonly class OrderItemData extends PagarmeData
+final readonly class ItemData extends PagarmeData
 {
     public function __construct(
         public string  $code,

+ 9 - 14
app/Data/Pagarme/Request/OrderRequestData/OrderPaymentData/OrderPaymentData.php → app/Data/Pagarme/Order/Parts/Request/PaymentData.php

@@ -1,24 +1,21 @@
 <?php
 
-namespace App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData;
+namespace App\Data\Pagarme\Order\Parts\Request;
 
 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
+final readonly class PaymentData extends PagarmeData
 {
     /**
-     * @param  OrderSplitData[]|null  $split
+     * @param  SplitData[]|null  $split
      */
-
     public function __construct(
         public string $paymentMethod,
 
         public ?array $split = null,
 
-        public ?OrderCreditCardData $creditCard = null,
-        public ?OrderPixData        $pix        = null,
+        public ?CreditCardData $creditCard = null,
+        public ?PixData        $pix        = null,
     ) {
         self::requireIn($this->paymentMethod, ['credit_card', 'pix'], 'payments.payment_method');
 
@@ -32,10 +29,9 @@ final readonly class OrderPaymentData extends PagarmeData
     }
 
     /**
-     * @param  OrderSplitData[]|null  $split
+     * @param  SplitData[]|null  $split
      */
-
-    public static function creditCard(OrderCreditCardData $creditCard, ?array $split = null): self
+    public static function creditCard(CreditCardData $creditCard, ?array $split = null): self
     {
         return new self(
             paymentMethod: 'credit_card',
@@ -45,10 +41,9 @@ final readonly class OrderPaymentData extends PagarmeData
     }
 
     /**
-     * @param  OrderSplitData[]|null  $split
+     * @param  SplitData[]|null  $split
      */
-
-    public static function pix(OrderPixData $pix, ?array $split = null): self
+    public static function pix(PixData $pix, ?array $split = null): self
     {
         return new self(
             paymentMethod: 'pix',

+ 2 - 2
app/Data/Pagarme/Request/OrderRequestData/OrderPaymentData/OrderPixData/OrderPixAdditionalInformationData.php → app/Data/Pagarme/Order/Parts/Request/PixAdditionalInformationData.php

@@ -1,10 +1,10 @@
 <?php
 
-namespace App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData\OrderPixData;
+namespace App\Data\Pagarme\Order\Parts\Request;
 
 use App\Data\Pagarme\PagarmeData;
 
-final readonly class OrderPixAdditionalInformationData extends PagarmeData
+final readonly class PixAdditionalInformationData extends PagarmeData
 {
     public function __construct(
         public string $name,

+ 3 - 3
app/Data/Pagarme/Request/OrderRequestData/OrderPaymentData/OrderPixData/OrderPixData.php → app/Data/Pagarme/Order/Parts/Request/PixData.php

@@ -1,13 +1,13 @@
 <?php
 
-namespace App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData\OrderPixData;
+namespace App\Data\Pagarme\Order\Parts\Request;
 
 use App\Data\Pagarme\PagarmeData;
 
-final readonly class OrderPixData extends PagarmeData
+final readonly class PixData extends PagarmeData
 {
     /**
-     * @param  OrderPixAdditionalInformationData[]|null  $additionalInformation
+     * @param  PixAdditionalInformationData[]|null  $additionalInformation
      */
 
     public function __construct(

+ 3 - 3
app/Data/Pagarme/Request/OrderRequestData/OrderPaymentData/OrderSplitData/OrderSplitData.php → app/Data/Pagarme/Order/Parts/Request/SplitData.php

@@ -1,17 +1,17 @@
 <?php
 
-namespace App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData\OrderSplitData;
+namespace App\Data\Pagarme\Order\Parts\Request;
 
 use App\Data\Pagarme\PagarmeData;
 
-final readonly class OrderSplitData extends PagarmeData
+final readonly class SplitData extends PagarmeData
 {
     public function __construct(
         public int    $amount,
         public string $recipientId,
         public string $type = 'flat',
 
-        public ?OrderSplitOptionsData $options = null,
+        public ?SplitOptionsData $options = null,
     ) {
         self::requirePositiveInt($this->amount, 'split.amount');
         self::requireFilled($this->recipientId, 'split.recipient_id');

+ 2 - 2
app/Data/Pagarme/Request/OrderRequestData/OrderPaymentData/OrderSplitData/OrderSplitOptionsData.php → app/Data/Pagarme/Order/Parts/Request/SplitOptionsData.php

@@ -1,10 +1,10 @@
 <?php
 
-namespace App\Data\Pagarme\Request\OrderRequestData\OrderPaymentData\OrderSplitData;
+namespace App\Data\Pagarme\Order\Parts\Request;
 
 use App\Data\Pagarme\PagarmeData;
 
-final readonly class OrderSplitOptionsData extends PagarmeData
+final readonly class SplitOptionsData extends PagarmeData
 {
     public function __construct(
         public bool $chargeProcessingFee = false,

+ 5 - 5
app/Data/Pagarme/Response/OrderResponseData/OrderChargeResponseData/OrderChargeResponseData.php → app/Data/Pagarme/Order/Parts/Response/ChargeData.php

@@ -1,10 +1,10 @@
 <?php
 
-namespace App\Data\Pagarme\Response\OrderResponseData\OrderChargeResponseData;
+namespace App\Data\Pagarme\Order\Parts\Response;
 
 use App\Data\Pagarme\PagarmeResponseData;
 
-final readonly class OrderChargeResponseData extends PagarmeResponseData
+final readonly class ChargeData extends PagarmeResponseData
 {
     public function __construct(
         public ?string $id,
@@ -15,10 +15,10 @@ final readonly class OrderChargeResponseData extends PagarmeResponseData
         public ?string $createdAt,
         public ?string $expiresAt = null,
 
-        public ?OrderTransactionResponseData $lastTransaction,
+        public ?TransactionData $lastTransaction,
     ) {}
 
-    public function transaction(): ?OrderTransactionResponseData
+    public function transaction(): ?TransactionData
     {
         return $this->lastTransaction;
     }
@@ -37,7 +37,7 @@ final readonly class OrderChargeResponseData extends PagarmeResponseData
             expiresAt: static::arrString($payload, 'expires_at'),
 
             lastTransaction: ! empty($transaction)
-                ? OrderTransactionResponseData::fromArray($transaction)
+                ? TransactionData::fromArray($transaction)
                 : null,
         );
     }

+ 2 - 2
app/Data/Pagarme/Response/OrderResponseData/OrderCheckoutResponseData.php → app/Data/Pagarme/Order/Parts/Response/CheckoutData.php

@@ -1,10 +1,10 @@
 <?php
 
-namespace App\Data\Pagarme\Response\OrderResponseData;
+namespace App\Data\Pagarme\Order\Parts\Response;
 
 use App\Data\Pagarme\PagarmeResponseData;
 
-final readonly class OrderCheckoutResponseData extends PagarmeResponseData
+final readonly class CheckoutData extends PagarmeResponseData
 {
     public function __construct(
         public ?string $id,

+ 2 - 2
app/Data/Pagarme/Response/OrderResponseData/OrderItemResponseData.php → app/Data/Pagarme/Order/Parts/Response/ItemData.php

@@ -1,10 +1,10 @@
 <?php
 
-namespace App\Data\Pagarme\Response\OrderResponseData;
+namespace App\Data\Pagarme\Order\Parts\Response;
 
 use App\Data\Pagarme\PagarmeResponseData;
 
-final readonly class OrderItemResponseData extends PagarmeResponseData
+final readonly class ItemData extends PagarmeResponseData
 {
     public function __construct(
         public ?string $id,

+ 2 - 2
app/Data/Pagarme/Response/OrderResponseData/OrderChargeResponseData/OrderTransactionResponseData.php → app/Data/Pagarme/Order/Parts/Response/TransactionData.php

@@ -1,10 +1,10 @@
 <?php
 
-namespace App\Data\Pagarme\Response\OrderResponseData\OrderChargeResponseData;
+namespace App\Data\Pagarme\Order\Parts\Response;
 
 use App\Data\Pagarme\PagarmeResponseData;
 
-final readonly class OrderTransactionResponseData extends PagarmeResponseData
+final readonly class TransactionData extends PagarmeResponseData
 {
     public function __construct(
         public ?string $id,

+ 10 - 10
app/Data/Pagarme/Request/BankAccountUpdateRequestData.php → app/Data/Pagarme/Recipient/BankAccountUpdateRequestData.php

@@ -1,6 +1,6 @@
 <?php
 
-namespace App\Data\Pagarme\Request;
+namespace App\Data\Pagarme\Recipient;
 
 use App\Data\Pagarme\PagarmeData;
 
@@ -31,15 +31,15 @@ final readonly class BankAccountUpdateRequestData extends PagarmeData
     {
         return $this->filterFilledRecursive([
             'bank_account' => [
-                'holder_name'         => $this->holderName,
-                'holder_type'         => $this->holderType,
-                'holder_document'     => $this->holderDocument,
-                'bank'                => $this->bank,
-                'branch_number'       => $this->branchNumber,
-                'branch_check_digit'  => $this->branchCheckDigit,
-                'account_number'      => $this->accountNumber,
-                'account_check_digit' => $this->accountCheckDigit,
-                'type'                => $this->type,
+                'holder_name'          => $this->holderName,
+                'holder_type'          => $this->holderType,
+                'holder_document'      => $this->holderDocument,
+                'bank'                 => $this->bank,
+                'branch_number'        => $this->branchNumber,
+                'branch_check_digit'   => $this->branchCheckDigit,
+                'account_number'       => $this->accountNumber,
+                'account_check_digit'  => $this->accountCheckDigit,
+                'type'                 => $this->type,
             ],
         ]);
     }

+ 2 - 2
app/Data/Pagarme/Request/RecipientRequestData/RecipientRegisterInformationData/RecipientAddressData.php → app/Data/Pagarme/Recipient/Parts/Request/AddressData.php

@@ -1,10 +1,10 @@
 <?php
 
-namespace App\Data\Pagarme\Request\RecipientRequestData\RecipientRegisterInformationData;
+namespace App\Data\Pagarme\Recipient\Parts\Request;
 
 use App\Data\Pagarme\PagarmeData;
 
-final readonly class RecipientAddressData extends PagarmeData
+final readonly class AddressData extends PagarmeData
 {
     public function __construct(
         public string  $street,

+ 39 - 0
app/Data/Pagarme/Recipient/Parts/Request/AutomaticAnticipationSettingsData.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace App\Data\Pagarme\Recipient\Parts\Request;
+
+use App\Data\Pagarme\PagarmeData;
+
+final readonly class AutomaticAnticipationSettingsData extends PagarmeData
+{
+    public function __construct(
+        public bool    $enabled,
+        public ?string $type             = null,
+        public ?int    $volumePercentage = null,
+        public ?int    $delay            = null,
+        public ?array  $days             = null,
+    ) {
+        if ($this->type !== null) {
+            self::requireIn($this->type, ['full', '1025'], 'automatic_anticipation_settings.type');
+        }
+
+        if ($this->volumePercentage !== null && ($this->volumePercentage < 0 || $this->volumePercentage > 100)) {
+            throw new \InvalidArgumentException('automatic_anticipation_settings.volume_percentage deve estar entre 0 e 100.');
+        }
+
+        if ($this->delay !== null && $this->delay < 1) {
+            throw new \InvalidArgumentException('automatic_anticipation_settings.delay deve ser maior ou igual a um.');
+        }
+    }
+
+    public function toArray(): array
+    {
+        return $this->filterFilledRecursive([
+            'enabled'           => $this->enabled,
+            'type'              => $this->type,
+            'volume_percentage' => $this->volumePercentage,
+            'delay'             => $this->delay,
+            'days'              => $this->days,
+        ]);
+    }
+}

+ 11 - 11
app/Data/Pagarme/Request/RecipientRequestData/RecipientBankAccountData.php → app/Data/Pagarme/Recipient/Parts/Request/BankAccountData.php

@@ -1,10 +1,10 @@
 <?php
 
-namespace App\Data\Pagarme\Request\RecipientRequestData;
+namespace App\Data\Pagarme\Recipient\Parts\Request;
 
 use App\Data\Pagarme\PagarmeData;
 
-final readonly class RecipientBankAccountData extends PagarmeData
+final readonly class BankAccountData extends PagarmeData
 {
     public function __construct(
         public string  $holderName,
@@ -30,15 +30,15 @@ final readonly class RecipientBankAccountData extends PagarmeData
     public function toArray(): array
     {
         return $this->filterFilledRecursive([
-            'holder_name'         => $this->holderName,
-            'holder_type'         => $this->holderType,
-            'holder_document'     => $this->holderDocument,
-            'bank'                => $this->bank,
-            'branch_number'       => $this->branchNumber,
-            'branch_check_digit'  => $this->branchCheckDigit,
-            'account_number'      => $this->accountNumber,
-            'account_check_digit' => $this->accountCheckDigit,
-            'type'                => $this->type,
+            'holder_name'          => $this->holderName,
+            'holder_type'          => $this->holderType,
+            'holder_document'      => $this->holderDocument,
+            'bank'                 => $this->bank,
+            'branch_number'        => $this->branchNumber,
+            'branch_check_digit'   => $this->branchCheckDigit,
+            'account_number'       => $this->accountNumber,
+            'account_check_digit'  => $this->accountCheckDigit,
+            'type'                 => $this->type,
         ]);
     }
 }

+ 2 - 2
app/Data/Pagarme/Request/RecipientRequestData/RecipientRegisterInformationData/RecipientPhoneNumbersData/RecipientPhoneData.php → app/Data/Pagarme/Recipient/Parts/Request/PhoneData.php

@@ -1,10 +1,10 @@
 <?php
 
-namespace App\Data\Pagarme\Request\RecipientRequestData\RecipientRegisterInformationData\RecipientPhoneNumbersData;
+namespace App\Data\Pagarme\Recipient\Parts\Request;
 
 use App\Data\Pagarme\PagarmeData;
 
-final readonly class RecipientPhoneData extends PagarmeData
+final readonly class PhoneData extends PagarmeData
 {
     public function __construct(
         public string $ddd,

+ 17 - 0
app/Data/Pagarme/Recipient/Parts/Request/PhoneNumbersData.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace App\Data\Pagarme\Recipient\Parts\Request;
+
+use App\Data\Pagarme\PagarmeData;
+
+final readonly class PhoneNumbersData extends PagarmeData
+{
+    public function __construct(
+        public PhoneData $phone,
+    ) {}
+
+    public function toArray(): array
+    {
+        return [$this->phone->toArray()];
+    }
+}

+ 4 - 5
app/Data/Pagarme/Request/RecipientRequestData/RecipientRegisterInformationData/RecipientRegisterInformationData.php → app/Data/Pagarme/Recipient/Parts/Request/RegisterInformationData.php

@@ -1,11 +1,10 @@
 <?php
 
-namespace App\Data\Pagarme\Request\RecipientRequestData\RecipientRegisterInformationData;
+namespace App\Data\Pagarme\Recipient\Parts\Request;
 
 use App\Data\Pagarme\PagarmeData;
-use App\Data\Pagarme\Request\RecipientRequestData\RecipientRegisterInformationData\RecipientPhoneNumbersData\RecipientPhoneNumbersData;
 
-final readonly class RecipientRegisterInformationData extends PagarmeData
+final readonly class RegisterInformationData extends PagarmeData
 {
     public function __construct(
         public string  $name,
@@ -16,8 +15,8 @@ final readonly class RecipientRegisterInformationData extends PagarmeData
         public int     $monthlyIncome,
         public string  $professionalOccupation,
 
-        public RecipientPhoneNumbersData $phoneNumbers,
-        public RecipientAddressData      $address,
+        public PhoneNumbersData $phoneNumbers,
+        public AddressData      $address,
     ) {
         self::requireFilled($this->name, 'register_information.name');
         self::requireFilled($this->email, 'register_information.email');

+ 2 - 2
app/Data/Pagarme/Request/RecipientRequestData/RecipientTransferSettingsData.php → app/Data/Pagarme/Recipient/Parts/Request/TransferSettingsData.php

@@ -1,10 +1,10 @@
 <?php
 
-namespace App\Data\Pagarme\Request\RecipientRequestData;
+namespace App\Data\Pagarme\Recipient\Parts\Request;
 
 use App\Data\Pagarme\PagarmeData;
 
-final readonly class RecipientTransferSettingsData extends PagarmeData
+final readonly class TransferSettingsData extends PagarmeData
 {
     public function __construct(
         public bool   $transferEnabled,

+ 2 - 2
app/Data/Pagarme/Response/RecipientResponseData/RecipientBankAccountResponseData.php → app/Data/Pagarme/Recipient/Parts/Response/BankAccountData.php

@@ -1,10 +1,10 @@
 <?php
 
-namespace App\Data\Pagarme\Response\RecipientResponseData;
+namespace App\Data\Pagarme\Recipient\Parts\Response;
 
 use App\Data\Pagarme\PagarmeResponseData;
 
-final readonly class RecipientBankAccountResponseData extends PagarmeResponseData
+final readonly class BankAccountData extends PagarmeResponseData
 {
     public function __construct(
         public ?string $holderName,

+ 9 - 6
app/Data/Pagarme/Request/RecipientRequestData/RecipientRequestData.php → app/Data/Pagarme/Recipient/RecipientRequestData.php

@@ -1,19 +1,22 @@
 <?php
 
-namespace App\Data\Pagarme\Request\RecipientRequestData;
+namespace App\Data\Pagarme\Recipient;
 
 use App\Data\Pagarme\PagarmeData;
-use App\Data\Pagarme\Request\RecipientRequestData\RecipientRegisterInformationData\RecipientRegisterInformationData;
+use App\Data\Pagarme\Recipient\Parts\Request\AutomaticAnticipationSettingsData;
+use App\Data\Pagarme\Recipient\Parts\Request\BankAccountData;
+use App\Data\Pagarme\Recipient\Parts\Request\RegisterInformationData;
+use App\Data\Pagarme\Recipient\Parts\Request\TransferSettingsData;
 
 final readonly class RecipientRequestData extends PagarmeData
 {
     public function __construct(
         public string $code,
 
-        public RecipientRegisterInformationData           $registerInformation,
-        public RecipientBankAccountData                   $defaultBankAccount,
-        public RecipientTransferSettingsData              $transferSettings,
-        public RecipientAutomaticAnticipationSettingsData $automaticAnticipationSettings,
+        public RegisterInformationData           $registerInformation,
+        public BankAccountData                   $defaultBankAccount,
+        public TransferSettingsData              $transferSettings,
+        public AutomaticAnticipationSettingsData $automaticAnticipationSettings,
     ) {
         self::requireFilled($this->code, 'code');
     }

+ 4 - 3
app/Data/Pagarme/Response/RecipientResponseData/RecipientResponseData.php → app/Data/Pagarme/Recipient/RecipientResponseData.php

@@ -1,8 +1,9 @@
 <?php
 
-namespace App\Data\Pagarme\Response\RecipientResponseData;
+namespace App\Data\Pagarme\Recipient;
 
 use App\Data\Pagarme\PagarmeResponseData;
+use App\Data\Pagarme\Recipient\Parts\Response\BankAccountData;
 
 final readonly class RecipientResponseData extends PagarmeResponseData
 {
@@ -14,7 +15,7 @@ final readonly class RecipientResponseData extends PagarmeResponseData
         public ?string $type,
         public ?string $status,
 
-        public ?RecipientBankAccountResponseData $defaultBankAccount = null,
+        public ?BankAccountData $defaultBankAccount = null,
 
         public ?string $createdAt = null,
         public ?string $updatedAt = null,
@@ -44,7 +45,7 @@ final readonly class RecipientResponseData extends PagarmeResponseData
             status:   static::arrString($payload, 'status'),
 
             defaultBankAccount: ! empty($bankAccount)
-                ? RecipientBankAccountResponseData::fromArray($bankAccount)
+                ? BankAccountData::fromArray($bankAccount)
                 : null,
 
             createdAt: static::arrString($payload, 'created_at'),

+ 0 - 19
app/Data/Pagarme/Request/RecipientRequestData/RecipientAutomaticAnticipationSettingsData.php

@@ -1,19 +0,0 @@
-<?php
-
-namespace App\Data\Pagarme\Request\RecipientRequestData;
-
-use App\Data\Pagarme\PagarmeData;
-
-final readonly class RecipientAutomaticAnticipationSettingsData extends PagarmeData
-{
-    public function __construct(
-        public bool $enabled,
-    ) {}
-
-    public function toArray(): array
-    {
-        return $this->filterFilledRecursive([
-            'enabled' => $this->enabled,
-        ]);
-    }
-}

+ 0 - 17
app/Data/Pagarme/Request/RecipientRequestData/RecipientRegisterInformationData/RecipientPhoneNumbersData/RecipientPhoneNumbersData.php

@@ -1,17 +0,0 @@
-<?php
-
-namespace App\Data\Pagarme\Request\RecipientRequestData\RecipientRegisterInformationData\RecipientPhoneNumbersData;
-
-use App\Data\Pagarme\PagarmeData;
-
-final readonly class RecipientPhoneNumbersData extends PagarmeData
-{
-    public function __construct(
-        public RecipientPhoneData $phone,
-    ) {}
-
-    public function toArray(): array
-    {
-        return [$this->phone->toArray()];
-    }
-}

+ 1 - 1
app/Data/Pagarme/Request/TransferRequestData.php → app/Data/Pagarme/Transfer/TransferRequestData.php

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

+ 1 - 1
app/Data/Pagarme/Response/TransferResponseData.php → app/Data/Pagarme/Transfer/TransferResponseData.php

@@ -1,6 +1,6 @@
 <?php
 
-namespace App\Data\Pagarme\Response;
+namespace App\Data\Pagarme\Transfer;
 
 use App\Data\Pagarme\PagarmeResponseData;
 

+ 14 - 0
app/Enums/CartStatusEnum.php

@@ -0,0 +1,14 @@
+<?php
+
+namespace App\Enums;
+
+use App\Traits\EnumHelper;
+
+enum CartStatusEnum: string
+{
+    use EnumHelper;
+
+    case OPEN      = 'open';
+    case PAID      = 'paid';
+    case CANCELLED = 'cancelled';
+}

+ 2 - 2
app/Http/Controllers/AddressController.php

@@ -41,7 +41,7 @@ class AddressController extends Controller
         return $this->successResponse(
             payload: new AddressResource($address),
             message: __('messages.created'),
-            code: 201,
+            code:    201,
         );
     }
 
@@ -61,7 +61,7 @@ class AddressController extends Controller
 
         return $this->successResponse(
             message: __('messages.deleted'),
-            code: 204,
+            code:    204,
         );
     }
 

+ 82 - 0
app/Http/Controllers/CartController.php

@@ -0,0 +1,82 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Http\Requests\CartRequest;
+use App\Http\Resources\CartResource;
+use App\Services\CartService;
+use Illuminate\Http\JsonResponse;
+use Illuminate\Support\Facades\Auth;
+
+class CartController extends Controller
+{
+    public function __construct(
+        protected CartService $service,
+    ) {}
+
+    public function index(): JsonResponse
+    {
+        $items = $this->service->getAll();
+
+        return $this->successResponse(payload: CartResource::collection($items));
+    }
+
+    public function store(CartRequest $request): JsonResponse
+    {
+        $validated = $request->validated();
+
+        $item = $this->hasSchedulePayload($validated)
+            ? $this->service->createWithSchedules($validated)
+            : $this->service->create($validated);
+
+        return $this->successResponse(payload: new CartResource($item), message: __('messages.created'), code: 201);
+    }
+
+    public function show(int $id): JsonResponse
+    {
+        $item = $this->service->findById($id);
+
+        return $this->successResponse(payload: new CartResource($item));
+    }
+
+    public function update(CartRequest $request, int $id): JsonResponse
+    {
+        $item = $this->service->update($id, $request->validated());
+
+        return $this->successResponse(payload: new CartResource($item), message: __('messages.updated'));
+    }
+
+    public function destroy(int $id): JsonResponse
+    {
+        $this->service->delete($id);
+
+        return $this->successResponse(message: __('messages.deleted'), code: 204);
+    }
+
+    //
+
+    public function myCarts(): JsonResponse
+    {
+        $items = $this->service->getByUserId(Auth::id());
+
+        return $this->successResponse(payload: CartResource::collection($items));
+    }
+
+    public function storeMine(CartRequest $request): JsonResponse
+    {
+        $request->merge(['client_id' => Auth::user()?->client?->id]);
+
+        $item = $this->service->createForUser($request->validated(), Auth::id());
+
+        return $this->successResponse(payload: new CartResource($item), message: __('messages.created'), code: 201);
+    }
+
+    //
+
+    private function hasSchedulePayload(array $data): bool
+    {
+        return ! empty($data['schedules'])
+            || ! empty($data['dates'])
+            || ! empty($data['date']);
+    }
+}

+ 50 - 0
app/Http/Controllers/CartItemController.php

@@ -0,0 +1,50 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Http\Requests\CartItemRequest;
+use App\Http\Resources\CartItemResource;
+use App\Services\CartItemService;
+use Illuminate\Http\JsonResponse;
+
+class CartItemController extends Controller
+{
+    public function __construct(
+        protected CartItemService $service,
+    ) {}
+
+    public function index(): JsonResponse
+    {
+        $items = $this->service->getAll();
+
+        return $this->successResponse(payload: CartItemResource::collection($items));
+    }
+
+    public function store(CartItemRequest $request): JsonResponse
+    {
+        $item = $this->service->create($request->validated());
+
+        return $this->successResponse(payload: new CartItemResource($item), message: __('messages.created'), code: 201);
+    }
+
+    public function show(int $id): JsonResponse
+    {
+        $item = $this->service->findById($id);
+
+        return $this->successResponse(payload: new CartItemResource($item));
+    }
+
+    public function update(CartItemRequest $request, int $id): JsonResponse
+    {
+        $item = $this->service->update($id, $request->validated());
+
+        return $this->successResponse(payload: new CartItemResource($item), message: __('messages.updated'));
+    }
+
+    public function destroy(int $id): JsonResponse
+    {
+        $this->service->delete($id);
+
+        return $this->successResponse(message: __('messages.deleted'), code: 204);
+    }
+}

+ 3 - 3
app/Http/Controllers/CityController.php

@@ -28,7 +28,7 @@ class CityController extends Controller
         return $this->successResponse(
             payload: new CityResource($item),
             message: __('messages.created'),
-            code: 201,
+            code:    201,
         );
     }
 
@@ -55,14 +55,14 @@ class CityController extends Controller
 
         return $this->successResponse(
             message: __('messages.deleted'),
-            code: 204,
+            code:    204,
         );
     }
 
     public function searchStateCityByDescription(Request $request): JsonResponse
     {
         $state = $request->query('stateUf');
-        $city = $request->query('cityName');
+        $city  = $request->query('cityName');
 
         $result = $this->service->findStateCityByDescription($state, $city);
 

+ 2 - 2
app/Http/Controllers/ClientController.php

@@ -29,7 +29,7 @@ class ClientController extends Controller
         return $this->successResponse(
             payload: new ClientResource($item),
             message: __('messages.created'),
-            code: 201,
+            code:    201,
         );
     }
 
@@ -56,7 +56,7 @@ class ClientController extends Controller
 
         return $this->successResponse(
             message: __('messages.deleted'),
-            code: 204,
+            code:    204,
         );
     }
 

+ 2 - 2
app/Http/Controllers/ClientFavoriteProviderController.php

@@ -38,7 +38,7 @@ class ClientFavoriteProviderController extends Controller
         return $this->successResponse(
             payload: new ClientFavoriteProviderResource($favorite),
             message: __('messages.created'),
-            code: 201,
+            code:    201,
         );
     }
 
@@ -58,7 +58,7 @@ class ClientFavoriteProviderController extends Controller
 
         return $this->successResponse(
             message: __('messages.deleted'),
-            code: 204,
+            code:    204,
         );
     }
 

+ 2 - 2
app/Http/Controllers/ClientPaymentMethodController.php

@@ -39,7 +39,7 @@ class ClientPaymentMethodController extends Controller
         return $this->successResponse(
             payload: new ClientPaymentMethodResource($paymentMethod),
             message: __('messages.created'),
-            code: 201,
+            code:    201,
         );
     }
 
@@ -63,7 +63,7 @@ class ClientPaymentMethodController extends Controller
 
         return $this->successResponse(
             message: __('messages.deleted'),
-            code: 204,
+            code:    204,
         );
     }
 }

+ 2 - 2
app/Http/Controllers/ClientProviderBlockController.php

@@ -29,7 +29,7 @@ class ClientProviderBlockController extends Controller
         return $this->successResponse(
             payload: new ClientProviderBlockResource($block),
             message: __('messages.created'),
-            code: 201,
+            code:    201,
         );
     }
 
@@ -39,7 +39,7 @@ class ClientProviderBlockController extends Controller
 
         return $this->successResponse(
             message: __('messages.deleted'),
-            code: 204,
+            code:    204,
         );
     }
 

+ 2 - 2
app/Http/Controllers/CountryController.php

@@ -27,7 +27,7 @@ class CountryController extends Controller
         return $this->successResponse(
             payload: new CountryResource($item),
             message: __('messages.created'),
-            code: 201,
+            code:    201,
         );
     }
 
@@ -54,7 +54,7 @@ class CountryController extends Controller
 
         return $this->successResponse(
             message: __('messages.deleted'),
-            code: 204,
+            code:    204,
         );
     }
 }

+ 2 - 2
app/Http/Controllers/MediaController.php

@@ -27,7 +27,7 @@ class MediaController extends Controller
         return $this->successResponse(
             payload: new MediaResource($item),
             message: __('messages.created'),
-            code: 201,
+            code:    201,
         );
     }
 
@@ -54,7 +54,7 @@ class MediaController extends Controller
 
         return $this->successResponse(
             message: __('messages.deleted'),
-            code: 204,
+            code:    204,
         );
     }
 }

+ 57 - 24
app/Http/Controllers/PaymentController.php

@@ -31,10 +31,54 @@ class PaymentController extends Controller
         return $this->successResponse(
             payload: new PaymentResource($item),
             message: $this->paymentMessage($item->status->value),
-            code: 201,
+            code:    201,
         );
     }
 
+    public function show(int $id): JsonResponse
+    {
+        $item = $this->service->findById($id);
+
+        return $this->successResponse(payload: new PaymentResource($item));
+    }
+
+    public function update(PaymentRequest $request, int $id): JsonResponse
+    {
+        $item = $this->service->update($id, $request->validated());
+
+        return $this->successResponse(payload: new PaymentResource($item), message: __('messages.updated'));
+    }
+
+    public function destroy(int $id): JsonResponse
+    {
+        $this->service->delete($id);
+
+        return $this->successResponse(message: __('messages.deleted'), code: 204);
+    }
+
+    //
+
+    /*
+    public function payCart(Request $request): JsonResponse
+    {
+        $validated = $request->validate([
+            'cart_id'                  => ['required', 'integer', 'exists:carts,id'],
+            'payment_method'           => ['required', 'in:credit_card,pix'],
+            'client_payment_method_id' => ['nullable', 'integer', 'exists:client_payment_methods,id'],
+            'card_id'                  => ['nullable', 'string', 'max:255'],
+            'phone'                    => ['nullable', 'string', 'max:20'],
+        ]);
+
+        $items = $this->service->payCart($validated, Auth::id());
+
+        return $this->successResponse(
+            payload: PaymentResource::collection($items),
+            message: __('messages.created'),
+            code:    201,
+        );
+    }
+    */
+
     public function paySchedule(Request $request, Schedule $schedule): JsonResponse
     {
         $validated = $request->validate([
@@ -49,9 +93,10 @@ class PaymentController extends Controller
         }
 
         $item = $this->service->payAcceptedSchedule(
-            schedule: $schedule,
-            paymentMethod: $validated['payment_method'],
+            schedule:              $schedule,
+            paymentMethod:         $validated['payment_method'],
             clientPaymentMethodId: $validated['client_payment_method_id'] ?? null,
+
             options: [
                 'phone'   => $validated['phone'] ?? null,
                 'card_id' => $validated['card_id'] ?? null,
@@ -68,10 +113,12 @@ class PaymentController extends Controller
         return $this->successResponse(
             payload: new PaymentResource($item),
             message: $this->paymentMessage($item->status->value),
-            code: 201,
+            code:    201,
         );
     }
 
+    //
+
     public function getSchedulePix(Schedule $schedule): JsonResponse
     {
         if ($schedule->client?->user_id !== Auth::id()) {
@@ -83,34 +130,20 @@ class PaymentController extends Controller
         return $this->successResponse(payload: new PaymentResource($item));
     }
 
-    public function show(int $id): JsonResponse
+    public function platformFees(): JsonResponse
     {
-        $item = $this->service->findById($id);
-
-        return $this->successResponse(payload: new PaymentResource($item));
+        return $this->successResponse(payload: $this->service->platformFees());
     }
 
-    public function update(PaymentRequest $request, int $id): JsonResponse
-    {
-        $item = $this->service->update($id, $request->validated());
-
-        return $this->successResponse(payload: new PaymentResource($item), message: __('messages.updated'));
-    }
-
-    public function destroy(int $id): JsonResponse
-    {
-        $this->service->delete($id);
-
-        return $this->successResponse(message: __('messages.deleted'), code: 204);
-    }
+    //
 
     private function paymentMessage(string $status): string
     {
         return match ($status) {
-            'paid' => __('messages.payment_confirmed'),
+            'paid'                                => __('messages.payment_confirmed'),
             'authorized', 'processing', 'pending' => __('messages.payment_pending_confirmation'),
-            'failed' => __('messages.payment_not_confirmed'),
-            default  => __('messages.created'),
+            'failed'                              => __('messages.payment_not_confirmed'),
+            default                               => __('messages.created'),
         };
     }
 }

+ 2 - 2
app/Http/Controllers/PermissionController.php

@@ -27,7 +27,7 @@ class PermissionController extends Controller
         return $this->successResponse(
             payload: new PermissionResource($item),
             message: __('messages.created'),
-            code: 201,
+            code:    201,
         );
     }
 
@@ -54,7 +54,7 @@ class PermissionController extends Controller
 
         return $this->successResponse(
             message: __('messages.deleted'),
-            code: 204,
+            code:    204,
         );
     }
 }

+ 2 - 2
app/Http/Controllers/ProviderBlockedDayController.php

@@ -38,7 +38,7 @@ class ProviderBlockedDayController extends Controller
         return $this->successResponse(
             payload: new ProviderBlockedDayResource($blockedDay),
             message: __('messages.created'),
-            code: 201,
+            code:    201,
         );
     }
 
@@ -62,7 +62,7 @@ class ProviderBlockedDayController extends Controller
 
         return $this->successResponse(
             message: __('messages.deleted'),
-            code: 204,
+            code:    204,
         );
     }
 }

+ 2 - 2
app/Http/Controllers/ProviderClientBlockController.php

@@ -29,7 +29,7 @@ class ProviderClientBlockController extends Controller
         return $this->successResponse(
             payload: new ProviderClientBlockResource($block),
             message: __('messages.created'),
-            code: 201,
+            code:    201,
         );
     }
 
@@ -39,7 +39,7 @@ class ProviderClientBlockController extends Controller
 
         return $this->successResponse(
             message: __('messages.deleted'),
-            code: 204,
+            code:    204,
         );
     }
 

+ 2 - 2
app/Http/Controllers/ProviderController.php

@@ -31,7 +31,7 @@ class ProviderController extends Controller
         return $this->successResponse(
             payload: new ProviderResource($item),
             message: __('messages.created'),
-            code: 201,
+            code:    201,
         );
     }
 
@@ -68,7 +68,7 @@ class ProviderController extends Controller
 
         return $this->successResponse(
             message: __('messages.deleted'),
-            code: 204,
+            code:    204,
         );
     }
 

+ 5 - 0
app/Http/Controllers/ProviderWithdrawalController.php

@@ -36,6 +36,11 @@ class ProviderWithdrawalController extends Controller
         ]);
     }
 
+    public function fees(): JsonResponse
+    {
+        return $this->successResponse(payload: $this->service->getWithdrawalFees());
+    }
+
     public function splits(): JsonResponse
     {
         $provider = $this->resolveProvider();

+ 1 - 1
app/Http/Controllers/ProviderWorkingDayController.php

@@ -37,7 +37,7 @@ class ProviderWorkingDayController extends Controller
 
         return $this->successResponse(
             message: __('messages.deleted'),
-            code: 204,
+            code:    204,
         );
     }
 }

+ 2 - 2
app/Http/Controllers/ReviewController.php

@@ -84,7 +84,7 @@ class ReviewController extends Controller
         return $this->successResponse(
             payload: new ReviewResource($review),
             message: __('messages.created'),
-            code: 201,
+            code:    201,
         );
     }
 
@@ -104,7 +104,7 @@ class ReviewController extends Controller
 
         return $this->successResponse(
             message: __('messages.deleted'),
-            code: 204,
+            code:    204,
         );
     }
 }

+ 2 - 2
app/Http/Controllers/ReviewImprovementController.php

@@ -29,7 +29,7 @@ class ReviewImprovementController extends Controller
         return $this->successResponse(
             payload: new ReviewImprovementResource($item),
             message: __('messages.created'),
-            code: 201,
+            code:    201,
         );
     }
 
@@ -39,7 +39,7 @@ class ReviewImprovementController extends Controller
 
         return $this->successResponse(
             message: __('messages.deleted'),
-            code: 204,
+            code:    204,
         );
     }
 }

+ 47 - 22
app/Http/Controllers/ScheduleController.php

@@ -7,6 +7,7 @@ use App\Http\Resources\ScheduleResource;
 use App\Services\ScheduleService;
 use Illuminate\Http\JsonResponse;
 use Illuminate\Http\Request;
+use Illuminate\Support\Arr;
 
 class ScheduleController extends Controller
 {
@@ -31,18 +32,35 @@ class ScheduleController extends Controller
         try {
             $validated = $request->validated();
 
-            $schedules = $this->scheduleService->createSingleOrMultiple($validated, $validated['schedules']);
+            $schedules = $this->scheduleService->createSingleOrMultiple(
+                Arr::except($validated, ['date', 'dates']),
+                $this->scheduleItems($validated),
+            );
 
             return $this->successResponse(
                 payload: ScheduleResource::collection($schedules),
                 message: count($schedules).' '.__('schedules.schedules_created'),
-                code: 201,
+                code:    201,
             );
         } catch (\Exception $e) {
             return $this->errorResponse($e->getMessage(), 422);
         }
     }
 
+    private function scheduleItems(array $data): array
+    {
+        if (! empty($data['dates'])) {
+            return array_map(
+                fn (string $date) => ['date' => $date],
+                $data['dates'],
+            );
+        }
+
+        return [
+            ['date' => $data['date']],
+        ];
+    }
+
     public function show(string $id): JsonResponse
     {
         $schedule = $this->scheduleService->getById($id);
@@ -72,10 +90,27 @@ class ScheduleController extends Controller
 
         return $this->successResponse(
             message: __('messages.deleted'),
-            code: 204,
+            code:    204,
         );
     }
 
+    //
+
+    public function clientProviderBlocks(Request $request): JsonResponse
+    {
+        $validated = $request->validate([
+            'client_id'   => 'required|integer|exists:clients,id',
+            'provider_id' => 'required|integer|exists:providers,id',
+        ]);
+
+        $blocks = $this->scheduleService->getClientProviderBlocks(
+            (int) $validated['client_id'],
+            (int) $validated['provider_id'],
+        );
+
+        return $this->successResponse(payload: $blocks);
+    }
+
     public function groupedByClient(): JsonResponse
     {
         $grouped = $this->scheduleService->getSchedulesDefaultGroupedByClient();
@@ -92,14 +127,16 @@ class ScheduleController extends Controller
         );
     }
 
-    public function updateStatus(string $id, ScheduleRequest $request): JsonResponse
+    //
+
+    public function cancelWithReason(string $id, Request $request): JsonResponse
     {
         try {
             $validated = $request->validate([
-                'status' => 'required|in:pending,accepted,rejected,paid,cancelled,started,finished',
+                'cancel_text' => 'required|string|min:5|max:1000',
             ]);
 
-            $schedule = $this->scheduleService->updateStatus($id, $validated['status']);
+            $schedule = $this->scheduleService->cancelWithReason((int) $id, $validated['cancel_text']);
 
             return $this->successResponse(
                 payload: new ScheduleResource($schedule),
@@ -110,14 +147,14 @@ class ScheduleController extends Controller
         }
     }
 
-    public function cancelWithReason(string $id, Request $request): JsonResponse
+    public function updateStatus(string $id, ScheduleRequest $request): JsonResponse
     {
         try {
             $validated = $request->validate([
-                'cancel_text' => 'required|string|min:5|max:1000',
+                'status' => 'required|in:pending,accepted,rejected,paid,cancelled,started,finished',
             ]);
 
-            $schedule = $this->scheduleService->cancelWithReason((int) $id, $validated['cancel_text']);
+            $schedule = $this->scheduleService->updateStatus($id, $validated['status']);
 
             return $this->successResponse(
                 payload: new ScheduleResource($schedule),
@@ -128,18 +165,6 @@ class ScheduleController extends Controller
         }
     }
 
-    public function clientProviderBlocks(Request $request): JsonResponse
-    {
-        $validated = $request->validate([
-            'client_id'   => 'required|integer|exists:clients,id',
-            'provider_id' => 'required|integer|exists:providers,id',
-        ]);
-
-        $blocks = $this->scheduleService->getClientProviderBlocks(
-            (int) $validated['client_id'],
-            (int) $validated['provider_id'],
-        );
+    //
 
-        return $this->successResponse(payload: $blocks);
-    }
 }

+ 2 - 2
app/Http/Controllers/StateController.php

@@ -27,7 +27,7 @@ class StateController extends Controller
         return $this->successResponse(
             payload: new StateResource($item),
             message: __('messages.created'),
-            code: 201,
+            code:    201,
         );
     }
 
@@ -54,7 +54,7 @@ class StateController extends Controller
 
         return $this->successResponse(
             message: __('messages.deleted'),
-            code: 204,
+            code:    204,
         );
     }
 }

+ 2 - 2
app/Http/Controllers/WebhookController.php

@@ -27,7 +27,7 @@ class WebhookController extends Controller
 
     private function validPagarmeCredentials(Request $request): bool
     {
-        $configuredUser = config('services.pagarme.webhook_user');
+        $configuredUser     = config('services.pagarme.webhook_user');
         $configuredPassword = config('services.pagarme.webhook_password');
 
         if (empty($configuredUser) || empty($configuredPassword)) {
@@ -41,7 +41,7 @@ class WebhookController extends Controller
 
     private function validBasicAuthCredentials(Request $request, string $configuredUser, string $configuredPassword): bool
     {
-        $receivedUser = $request->getUser();
+        $receivedUser     = $request->getUser();
         $receivedPassword = $request->getPassword();
 
         return is_string($receivedUser)

+ 33 - 0
app/Http/Requests/CartItemRequest.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace App\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class CartItemRequest extends FormRequest
+{
+    public function rules(): array
+    {
+        $rules = [
+            'cart_id'     => ['sometimes', 'integer', 'exists:carts,id'],
+            'schedule_id' => ['sometimes', 'integer', 'exists:schedules,id'],
+        ];
+
+        if ($this->isMethod('POST')) {
+            $rules['cart_id']     = ['required', 'integer', 'exists:carts,id'];
+            $rules['schedule_id'] = ['required', 'integer', 'exists:schedules,id'];
+        }
+
+        return $rules;
+    }
+
+    /**
+    * Add custom messages when needed
+    * public function messages(): array
+    * {
+    *   return [
+    *        'field.required' => __('message.algo'),
+    *    ];
+    * }
+    */
+}

+ 81 - 0
app/Http/Requests/CartRequest.php

@@ -0,0 +1,81 @@
+<?php
+
+namespace App\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class CartRequest extends FormRequest
+{
+    public function authorize(): bool
+    {
+        return true;
+    }
+
+    public function rules(): array
+    {
+        $rules = [
+            'client_id'                => ['sometimes', 'integer', 'exists:clients,id'],
+            'provider_id'              => ['sometimes', 'integer', 'exists:providers,id'],
+            'address_id'               => ['sometimes', 'integer', 'exists:addresses,id'],
+            'date'                     => ['nullable', 'date', 'after_or_equal:today'],
+            'dates'                    => ['nullable', 'array'],
+            'dates.*'                  => ['date', 'after_or_equal:today'],
+            'schedules'                => ['nullable', 'array'],
+            'schedules.*.provider_id'  => ['sometimes', 'integer', 'exists:providers,id'],
+            'schedules.*.date'         => ['required', 'date', 'after_or_equal:today'],
+            'schedules.*.period_type'  => ['required', 'in:2,4,6,8'],
+            'schedules.*.start_time'   => ['required', 'date_format:H:i'],
+            'schedules.*.end_time'     => ['required', 'date_format:H:i', 'after:schedules.*.start_time'],
+            'schedules.*.total_amount' => ['required', 'numeric', 'min:0'],
+            'schedules.*.offers_meal'  => ['nullable', 'boolean'],
+            'offers_meal'              => ['sometimes', 'nullable', 'boolean'],
+            'period_type'              => ['sometimes', 'in:2,4,6,8'],
+            'start_time'               => ['sometimes', 'date_format:H:i'],
+            'end_time'                 => ['sometimes', 'date_format:H:i', 'after:start_time'],
+            'total_amount'             => ['sometimes', 'numeric', 'min:0'],
+            'schedule_type'            => ['sometimes', 'in:default,custom'],
+        ];
+
+        if ($this->isMethod('POST')) {
+            $rules['client_id'] = ['required', 'integer', 'exists:clients,id'];
+
+            if ($this->hasSchedulePayload()) {
+                $rules['provider_id']              = ['required_without:schedules', 'integer', 'exists:providers,id'];
+                $rules['address_id']               = ['required', 'integer', 'exists:addresses,id'];
+                $rules['date']                     = ['required_without_all:dates,schedules', 'nullable', 'date', 'after_or_equal:today'];
+                $rules['dates']                    = ['nullable', 'array'];
+                $rules['dates.*']                  = ['date', 'after_or_equal:today'];
+                $rules['schedules']                = ['nullable', 'array', 'min:1'];
+                $rules['schedules.*.provider_id']  = ['required_without:provider_id', 'integer', 'exists:providers,id'];
+                $rules['schedules.*.date']         = ['required', 'date', 'after_or_equal:today'];
+                $rules['schedules.*.period_type']  = ['required', 'in:2,4,6,8'];
+                $rules['schedules.*.start_time']   = ['required', 'date_format:H:i'];
+                $rules['schedules.*.end_time']     = ['required', 'date_format:H:i', 'after:schedules.*.start_time'];
+                $rules['schedules.*.total_amount'] = ['required', 'numeric', 'min:0'];
+                $rules['period_type']              = ['required_without:schedules', 'in:2,4,6,8'];
+                $rules['start_time']               = ['required_without:schedules', 'date_format:H:i'];
+                $rules['end_time']                 = ['required_without:schedules', 'date_format:H:i', 'after:start_time'];
+                $rules['total_amount']             = ['required_without:schedules', 'numeric', 'min:0'];
+            }
+        }
+
+        return $rules;
+    }
+
+    private function hasSchedulePayload(): bool
+    {
+        return $this->filled('date')
+            || $this->filled('dates')
+            || $this->filled('schedules');
+    }
+
+    /**
+    * Add custom messages when needed
+    * public function messages(): array
+    * {
+    *   return [
+    *        'field.required' => __('message.algo'),
+    *    ];
+    * }
+    */
+}

+ 10 - 36
app/Http/Requests/ScheduleRequest.php

@@ -20,18 +20,12 @@ class ScheduleRequest extends FormRequest
             'date'                     => 'nullable|date|after_or_equal:today',
             'dates'                    => 'nullable|array',
             'dates.*'                  => 'date|after_or_equal:today',
-            'schedules'                => 'nullable|array',
-            'schedules.*.date'         => 'required|date|after_or_equal:today',
-            'schedules.*.period_type'  => 'required|in:2,4,6,8',
-            'schedules.*.start_time'   => 'required|date_format:H:i',
-            'schedules.*.end_time'     => 'required|date_format:H:i|after:schedules.*.start_time',
-            'schedules.*.total_amount' => 'required|numeric|min:0',
-            'schedules.*.offers_meal'  => 'nullable|boolean',
             'offers_meal'              => 'sometimes|nullable|boolean',
             'period_type'              => 'sometimes|required|in:2,4,6,8',
             'schedule_type'            => 'sometimes|in:default,custom',
             'start_time'               => 'sometimes|required|date_format:H:i',
             'end_time'                 => 'sometimes|required|date_format:H:i|after:start_time',
+            'total_amount'             => 'sometimes|required|numeric|min:0',
             'status'                   => 'sometimes|in:pending,accepted,rejected,paid,cancelled,started,finished',
             'code_verified'            => 'sometimes|boolean',
         ];
@@ -40,18 +34,13 @@ class ScheduleRequest extends FormRequest
             $rules['client_id']                = 'required|exists:clients,id';
             $rules['provider_id']              = 'required|exists:providers,id';
             $rules['address_id']               = 'required|exists:addresses,id';
-            $rules['date']                     = 'required_without_all:dates,schedules|nullable|date|after_or_equal:today';
+            $rules['date']                     = 'required_without:dates|nullable|date|after_or_equal:today';
             $rules['dates']                    = 'nullable|array';
             $rules['dates.*']                  = 'date|after_or_equal:today';
-            $rules['schedules']                = 'nullable|array|min:1';
-            $rules['schedules.*.date']         = 'required|date|after_or_equal:today';
-            $rules['schedules.*.period_type']  = 'required|in:2,4,6,8';
-            $rules['schedules.*.start_time']   = 'required|date_format:H:i';
-            $rules['schedules.*.end_time']     = 'required|date_format:H:i|after:schedules.*.start_time';
-            $rules['schedules.*.total_amount'] = 'required|numeric|min:0';
-            $rules['period_type']              = 'required_without:schedules|in:2,4,6,8';
-            $rules['start_time']               = 'required_without:schedules|date_format:H:i';
-            $rules['end_time']                 = 'required_without:schedules|date_format:H:i|after:start_time';
+            $rules['period_type']              = 'required|in:2,4,6,8';
+            $rules['start_time']               = 'required|date_format:H:i';
+            $rules['end_time']                 = 'required|date_format:H:i|after:start_time';
+            $rules['total_amount']             = 'required|numeric|min:0';
             $rules['status']                   = 'in:pending';
         }
 
@@ -67,37 +56,22 @@ class ScheduleRequest extends FormRequest
             'provider_id.exists'                 => 'Prestador não encontrado.',
             'address_id.required'                => 'O endereço é obrigatório.',
             'address_id.exists'                  => 'Endereço não encontrado.',
-            'date.required_without_all'          => 'A data é obrigatória quando não há múltiplas datas ou agendamentos.',
+            'date.required_without'              => 'A data é obrigatória quando não há múltiplas datas.',
             'date.date'                          => 'Data inválida.',
             'date.after_or_equal'                => 'A data deve ser hoje ou futura.',
             'dates.array'                        => 'Datas devem ser um array.',
             'dates.*.date'                       => 'Uma das datas é inválida.',
             'dates.*.after_or_equal'             => 'Todas as datas devem ser hoje ou futuras.',
-            'schedules.array'                    => 'Agendamentos devem ser um array.',
-            'schedules.min'                      => 'É necessário pelo menos um agendamento.',
-            'schedules.*.date.required'          => 'A data do agendamento é obrigatória.',
-            'schedules.*.date.date'              => 'Data do agendamento inválida.',
-            'schedules.*.date.after_or_equal'    => 'A data do agendamento deve ser hoje ou futura.',
-            'schedules.*.period_type.required'   => 'O período do agendamento é obrigatório.',
-            'schedules.*.period_type.in'         => 'Período do agendamento inválido.',
-            'schedules.*.start_time.required'    => 'O horário de início do agendamento é obrigatório.',
-            'schedules.*.start_time.date_format' => 'Formato de horário de início inválido.',
-            'schedules.*.end_time.required'      => 'O horário de término do agendamento é obrigatório.',
-            'schedules.*.end_time.date_format'   => 'Formato de horário de término inválido.',
-            'schedules.*.end_time.after'         => 'O horário de término deve ser após o horário de início.',
-            'schedules.*.total_amount.required'  => 'O valor total do agendamento é obrigatório.',
-            'schedules.*.total_amount.numeric'   => 'O valor total deve ser numérico.',
-            'schedules.*.total_amount.min'       => 'O valor total deve ser maior ou igual a zero.',
             'period_type.required'               => 'O período é obrigatório.',
-            'period_type.required_without'       => 'O período é obrigatório quando não há agendamentos múltiplos.',
             'period_type.in'                     => 'Período inválido.',
             'start_time.required'                => 'O horário de início é obrigatório.',
-            'start_time.required_without'        => 'O horário de início é obrigatório quando não há agendamentos múltiplos.',
             'start_time.date_format'             => 'Formato de horário inválido.',
             'end_time.required'                  => 'O horário de término é obrigatório.',
-            'end_time.required_without'          => 'O horário de término é obrigatório quando não há agendamentos múltiplos.',
             'end_time.date_format'               => 'Formato de horário inválido.',
             'end_time.after'                     => 'O horário de término deve ser após o horário de início.',
+            'total_amount.required'              => 'O valor total é obrigatório.',
+            'total_amount.numeric'               => 'O valor total deve ser numérico.',
+            'total_amount.min'                   => 'O valor total deve ser maior ou igual a zero.',
             'status.in'                          => 'Status inválido.',
         ];
     }

+ 37 - 0
app/Http/Resources/CartItemResource.php

@@ -0,0 +1,37 @@
+<?php
+
+namespace App\Http\Resources;
+
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
+use App\Models\CartItem;
+
+class CartItemResource extends JsonResource
+{
+    /**
+     * Transform the resource into an array.
+     *
+     * @return array<string, mixed>
+     */
+    public function toArray(Request $request): array
+    {
+        return [
+            'id'          => $this->id,
+            'cart_id'     => $this->cart_id,
+            'schedule_id' => $this->schedule_id,
+            'schedule'    => new ScheduleResource($this->whenLoaded('schedule')),
+            'created_at'  => $this->created_at?->toISOString(),
+            'updated_at'  => $this->updated_at?->toISOString(),
+        ];
+    }
+
+    /**
+     * @param \Illuminate\Database\Eloquent\Collection<CartItem> $resource
+     * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection<CartItemResource>
+     */
+    public static function collection($resource): AnonymousResourceCollection
+    {
+        return parent::collection($resource);
+    }
+}

+ 43 - 0
app/Http/Resources/CartResource.php

@@ -0,0 +1,43 @@
+<?php
+
+namespace App\Http\Resources;
+
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
+use App\Models\Cart;
+
+class CartResource extends JsonResource
+{
+    /**
+     * Transform the resource into an array.
+     *
+     * @return array<string, mixed>
+     */
+    public function toArray(Request $request): array
+    {
+        return [
+            'id'           => $this->id,
+            'client_id'    => $this->client_id,
+            'status'       => $this->status?->value,
+            'schedule_ids' => $this->whenLoaded('items', fn () => $this->items->pluck('schedule_id')->values()),
+            'items'        => $this->whenLoaded('items', fn () => CartItemResource::collection($this->items)),
+
+            'schedules' => $this->whenLoaded('items', fn () => ScheduleResource::collection(
+                $this->items->pluck('schedule')->filter()->values(),
+            )),
+
+            'created_at'   => $this->created_at?->toISOString(),
+            'updated_at'   => $this->updated_at?->toISOString(),
+        ];
+    }
+
+    /**
+     * @param \Illuminate\Database\Eloquent\Collection<Cart> $resource
+     * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection<CartResource>
+     */
+    public static function collection($resource): AnonymousResourceCollection
+    {
+        return parent::collection($resource);
+    }
+}

+ 6 - 4
app/Http/Resources/PaymentResource.php

@@ -19,6 +19,7 @@ class PaymentResource extends JsonResource
         return [
             'id'                          => $this->id,
             'schedule_id'                 => $this->schedule_id,
+            'schedule'                    => new ScheduleResource($this->whenLoaded('schedule')),
             'client_id'                   => $this->client_id,
             'provider_id'                 => $this->provider_id,
             'client_name'                 => $this->client?->user?->name,
@@ -67,18 +68,19 @@ class PaymentResource extends JsonResource
             return null;
         }
 
-        $charge = $this->gateway_payload['charges'][0] ?? [];
-        $transaction = $charge['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'     => $transaction['qr_code']     ?? null,
             'qr_code_url' => $transaction['qr_code_url'] ?? null,
             'expires_at'  => $expiresAt,
-            'status'      => $transaction['status'] ?? null,
+            'status'      => $transaction['status']      ?? null,
         ];
     }
+
 }

+ 25 - 14
app/Http/Resources/ScheduleResource.php

@@ -2,6 +2,7 @@
 
 namespace App\Http\Resources;
 
+use App\Models\CartItem;
 use Illuminate\Http\Request;
 use Illuminate\Http\Resources\Json\JsonResource;
 
@@ -9,6 +10,14 @@ class ScheduleResource extends JsonResource
 {
     public function toArray(Request $request): array
     {
+        $cartId = $this->cart_id ?? CartItem::query()
+            ->where('schedule_id', $this->id)
+            ->value('cart_id');
+
+        $cartItemsCount = $this->cart_items_count ?? (
+            $cartId ? CartItem::query()->where('cart_id', $cartId)->count() : 0
+        );
+
         return [
             'id'            => $this->id,
             'client_id'     => $this->client_id,
@@ -24,20 +33,22 @@ class ScheduleResource extends JsonResource
                 $this->address->city ? "{$this->address->city->name}/{$this->address->state?->code}" : null,
             ])) : null,
 
-            'address'       => new AddressResource($this->whenLoaded('address')),
-            'date'          => $this->date?->format('Y-m-d'),
-            'period_type'   => $this->period_type,
-            'schedule_type' => $this->schedule_type,
-            'start_time'    => $this->start_time,
-            'end_time'      => $this->end_time,
-            'offers_meal'   => $this->offers_meal,
-            'status'        => $this->status,
-            'total_amount'  => $this->total_amount,
-            'code'          => $this->code,
-            'code_verified' => $this->code_verified,
-            'created_at'    => $this->created_at?->toISOString(),
-            'updated_at'    => $this->updated_at?->toISOString(),
-            'deleted_at'    => $this->deleted_at?->toISOString(),
+            'address'          => new AddressResource($this->whenLoaded('address')),
+            'date'             => $this->date?->format('Y-m-d'),
+            'period_type'      => $this->period_type,
+            'schedule_type'    => $this->schedule_type,
+            'start_time'       => $this->start_time,
+            'end_time'         => $this->end_time,
+            'offers_meal'      => $this->offers_meal,
+            'status'           => $this->status,
+            'total_amount'     => $this->total_amount,
+            'code'             => $this->code,
+            'code_verified'    => $this->code_verified,
+            'cart_id'          => $cartId,
+            'cart_items_count' => $cartItemsCount,
+            'created_at'       => $this->created_at?->toISOString(),
+            'updated_at'       => $this->updated_at?->toISOString(),
+            'deleted_at'       => $this->deleted_at?->toISOString(),
         ];
     }
 }

+ 35 - 12
app/Jobs/FinishScheduleJob.php

@@ -4,6 +4,7 @@ namespace App\Jobs;
 
 use App\Models\Address;
 use App\Models\Client;
+use App\Models\Payment;
 use App\Models\Provider;
 use App\Models\Schedule;
 use App\Models\User;
@@ -28,6 +29,7 @@ class FinishScheduleJob implements ShouldQueue
     {
         try {
             $schedule = Schedule::find($this->scheduleId);
+
             $date_cleaned = Carbon::parse($schedule->date)->format('Y-m-d');
 
             if (! $schedule) {
@@ -35,6 +37,7 @@ class FinishScheduleJob implements ShouldQueue
             }
 
             Log::channel('schedule_end_jobs')->info('Verificando status do agendamento id: '.$schedule->id);
+
             Log::channel('schedule_end_jobs')->info('Status do agendamento: '.$schedule->status);
 
             if ($schedule->status !== 'started') {
@@ -42,7 +45,9 @@ class FinishScheduleJob implements ShouldQueue
             }
 
             Log::channel('schedule_end_jobs')->info('Verificando data');
+
             Log::channel('schedule_end_jobs')->info('Data do agendamento: '.$date_cleaned);
+
             Log::channel('schedule_end_jobs')->info('Data atual: '.now()->toDateString());
 
             if ($date_cleaned > now()->toDateString()) {
@@ -50,7 +55,9 @@ class FinishScheduleJob implements ShouldQueue
             }
 
             Log::channel('schedule_end_jobs')->info('Verificando horário');
+
             Log::channel('schedule_end_jobs')->info('Horário do agendamento: '.$schedule->end_time);
+
             Log::channel('schedule_end_jobs')->info('Horário atual: '.now()->toTimeString());
 
             $end_date_time = Carbon::parse($date_cleaned.' '.$schedule->end_time);
@@ -78,25 +85,41 @@ class FinishScheduleJob implements ShouldQueue
             ]);
 
             $emailService = new EmailService;
+
             $serviceAmount = (float) $schedule->total_amount;
-            $serviceFee = $serviceAmount * 0.11;
+
+            $payment = Payment::query()
+                ->where('schedule_id', $schedule->id)
+                ->whereIn('status', ['paid', 'authorized'])
+                ->latest('id')
+                ->first();
+
+            $paymentMethod = $payment?->payment_method ?? 'pix';
+
+            $platformFeeRate = $paymentMethod === 'credit_card'
+                ? (float) config('services.pagarme.platform_credit_card_fee_rate')
+                : (float) config('services.pagarme.platform_pix_fee_rate');
+
+            $serviceFee = round($serviceAmount * $platformFeeRate, 2);
+
             $finalAmount = $serviceAmount + $serviceFee;
 
             $email_cliente = User::find($schedule->client->user_id)->email;
+
             $address = Address::find($schedule->address_id);
 
             $emailService->sendEmailReceipt(
-                email: $email_cliente,
-                schedule: $schedule,
-                client_name: $schedule->client->user->name,
-                service_date: $schedule->date,
-                start_time: $schedule->start_time,
-                end_time: $schedule->end_time,
-                address: $address->address.', '.$address->number.($address->has_complement ? ', '.$address->complement : '').' - '.$address->district.', '.$address->city->name.'/'.$address->state->code,
-                total_amount: $serviceAmount,
-                service_fee: $serviceFee,
-                final_amount: $finalAmount,
-                payment_method: 'PIX'
+                email:          $email_cliente,
+                schedule:       $schedule,
+                client_name:    $schedule->client->user->name,
+                service_date:   $schedule->date,
+                start_time:     $schedule->start_time,
+                end_time:       $schedule->end_time,
+                address:        $address->address.', '.$address->number.($address->has_complement ? ', '.$address->complement : '').' - '.$address->district.', '.$address->city->name.'/'.$address->state->code,
+                total_amount:   $serviceAmount,
+                service_fee:    $serviceFee,
+                final_amount:   $finalAmount,
+                payment_method: $paymentMethod === 'credit_card' ? 'Cartão de crédito' : 'PIX'
             );
         } catch (\Exception $e) {
             Log::channel('schedule_end_jobs')->error('Erro ao finalizar agendamento id: '.$this->scheduleId.'. Erro: '.$e->getMessage());

+ 2 - 0
app/Jobs/StartScheduleJob.php

@@ -23,6 +23,7 @@ class StartScheduleJob implements ShouldQueue
     {
         try {
             $schedule = Schedule::find($this->scheduleId);
+
             $date_cleaned = Carbon::parse($schedule->date)->format('Y-m-d');
 
             if (! $schedule) {
@@ -43,6 +44,7 @@ class StartScheduleJob implements ShouldQueue
             if ($date_cleaned > now()->toDateString()) {
                 return;
             }
+
             Log::channel('schedule_start_jobs')->info('Verificando horário');
             Log::channel('schedule_start_jobs')->info('Horário do agendamento: '.$schedule->start_time);
             Log::channel('schedule_start_jobs')->info('Horário atual: '.now()->toTimeString());

+ 26 - 26
app/Models/Address.php

@@ -31,32 +31,32 @@ use Illuminate\Support\Facades\DB;
  * @property float|null $longitude
  * @property-read \App\Models\City|null $city
  * @property-read \App\Models\State|null $state
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Address newModelQuery()
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Address newQuery()
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Address onlyTrashed()
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Address query()
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Address whereAddress($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Address whereAddressType($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Address whereCityId($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Address whereComplement($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Address whereCreatedAt($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Address whereDeletedAt($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Address whereDistrict($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Address whereHasComplement($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Address whereId($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Address whereInstructions($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Address whereIsPrimary($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Address whereLatitude($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Address whereLongitude($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Address whereNickname($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Address whereNumber($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Address whereSource($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Address whereSourceId($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Address whereStateId($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Address whereUpdatedAt($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Address whereZipCode($value)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Address withTrashed(bool $withTrashed = true)
- * @method static \Illuminate\Database\Eloquent\Builder<static>|Address withoutTrashed()
+ * @method static Builder<static>|Address newModelQuery()
+ * @method static Builder<static>|Address newQuery()
+ * @method static Builder<static>|Address onlyTrashed()
+ * @method static Builder<static>|Address query()
+ * @method static Builder<static>|Address whereAddress($value)
+ * @method static Builder<static>|Address whereAddressType($value)
+ * @method static Builder<static>|Address whereCityId($value)
+ * @method static Builder<static>|Address whereComplement($value)
+ * @method static Builder<static>|Address whereCreatedAt($value)
+ * @method static Builder<static>|Address whereDeletedAt($value)
+ * @method static Builder<static>|Address whereDistrict($value)
+ * @method static Builder<static>|Address whereHasComplement($value)
+ * @method static Builder<static>|Address whereId($value)
+ * @method static Builder<static>|Address whereInstructions($value)
+ * @method static Builder<static>|Address whereIsPrimary($value)
+ * @method static Builder<static>|Address whereLatitude($value)
+ * @method static Builder<static>|Address whereLongitude($value)
+ * @method static Builder<static>|Address whereNickname($value)
+ * @method static Builder<static>|Address whereNumber($value)
+ * @method static Builder<static>|Address whereSource($value)
+ * @method static Builder<static>|Address whereSourceId($value)
+ * @method static Builder<static>|Address whereStateId($value)
+ * @method static Builder<static>|Address whereUpdatedAt($value)
+ * @method static Builder<static>|Address whereZipCode($value)
+ * @method static Builder<static>|Address withTrashed(bool $withTrashed = true)
+ * @method static Builder<static>|Address withoutTrashed()
  * @mixin \Eloquent
  */
 class Address extends Model

+ 58 - 0
app/Models/Cart.php

@@ -0,0 +1,58 @@
+<?php
+
+namespace App\Models;
+
+use App\Enums\CartStatusEnum;
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\SoftDeletes;
+
+/**
+ * @property CartStatusEnum $status
+ * @property-read \App\Models\Client|null $client
+ * @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\CartItem> $items
+ * @property-read int|null $items_count
+ * @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Schedule> $schedules
+ * @property-read int|null $schedules_count
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|Cart newModelQuery()
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|Cart newQuery()
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|Cart onlyTrashed()
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|Cart query()
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|Cart withTrashed(bool $withTrashed = true)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|Cart withoutTrashed()
+ * @mixin \Eloquent
+ */
+class Cart extends Model
+{
+    use HasFactory, SoftDeletes;
+
+    protected $table = 'carts';
+
+    protected $fillable = [
+        'client_id',
+        'status',
+    ];
+
+    protected $casts = [
+        'status'     => CartStatusEnum::class,
+        'created_at' => 'datetime',
+        'updated_at' => 'datetime',
+        'deleted_at' => 'datetime',
+    ];
+
+    public function client()
+    {
+        return $this->belongsTo(Client::class);
+    }
+
+    public function schedules()
+    {
+        return $this->belongsToMany(Schedule::class, 'cart_items')
+            ->withTimestamps();
+    }
+
+    public function items()
+    {
+        return $this->hasMany(CartItem::class);
+    }
+}

+ 41 - 0
app/Models/CartItem.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+/**
+ * @property-read \App\Models\Cart|null $cart
+ * @property-read \App\Models\Schedule|null $schedule
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|CartItem newModelQuery()
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|CartItem newQuery()
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|CartItem query()
+ * @mixin \Eloquent
+ */
+class CartItem extends Model
+{
+    use HasFactory;
+
+    protected $table = 'cart_items';
+
+    protected $fillable = [
+        'cart_id',
+        'schedule_id',
+    ];
+
+    protected $casts = [
+        'created_at' => 'datetime',
+        'updated_at' => 'datetime',
+    ];
+
+    public function cart()
+    {
+        return $this->belongsTo(Cart::class);
+    }
+
+    public function schedule()
+    {
+        return $this->belongsTo(Schedule::class);
+    }
+}

+ 2 - 2
app/Models/Client.php

@@ -38,8 +38,8 @@ use Illuminate\Database\Eloquent\SoftDeletes;
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Client whereCreatedAt($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Client whereDeletedAt($value)
  * @method static \Illuminate\Database\Eloquent\Builder<static>|Client whereDocument($value)
- * @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 whereGatewayCustomerCode($value)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|Client whereGatewayCustomerId($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)

+ 38 - 33
app/Rules/ScheduleBusinessRules.php

@@ -15,16 +15,17 @@ use Illuminate\Support\Collection;
 class ScheduleBusinessRules
 {
     // Status que devem ser ignorados na validação de limite por semana
+
     private const EXCLUDED_STATUSES = ['cancelled', 'rejected'];
 
     /**
      * Valida se o prestador pode ter mais um agendamento com o cliente na semana
      * Limite: 2 agendamentos por semana (domingo a sábado)
      *
-     * @param  int  $clientId
-     * @param  int  $providerId
-     * @param  string  $date  (Y-m-d)
-     * @param  int|null  $excludeScheduleId
+     * @param  int      $clientId
+     * @param  int      $providerId
+     * @param  string   $date  (Y-m-d)
+     * @param  int|null $excludeScheduleId
      * @return bool
      *
      * @throws \Exception
@@ -34,7 +35,7 @@ class ScheduleBusinessRules
         $date = Carbon::parse($date);
 
         $weekStart = $date->copy()->startOfWeek(Carbon::SUNDAY);
-        $weekEnd = $date->copy()->endOfWeek(Carbon::SATURDAY);
+        $weekEnd   = $date->copy()->endOfWeek(Carbon::SATURDAY);
 
         $weeklySchedulesCount = Schedule::where('client_id', $clientId)
             ->where('provider_id', $providerId)
@@ -55,9 +56,9 @@ class ScheduleBusinessRules
     /**
      * Valida se o prestador tem horário de trabalho cadastrado para o dia da semana e período
      *
-     * @param  int  $provider_id
-     * @param  int  $day_of_week  (0 - domingo, 6 - sábado)
-     * @param  string  $period  ('morning' ou 'afternoon')
+     * @param  int    $provider_id
+     * @param  int    $day_of_week (0 - domingo, 6 - sábado)
+     * @param  string $period      ('morning' ou 'afternoon')
      * @return bool
      *
      * @throws \Exception
@@ -79,10 +80,10 @@ class ScheduleBusinessRules
     /**
      * Valida se o prestador tem bloqueio cadastrado para o dia e horário
      *
-     * @param  int  $provider_id
-     * @param  string  $date_ymd  (Y-m-d)
-     * @param  string  $start_time  (H:i:s)
-     * @param  string  $end_time  (H:i:s)
+     * @param  int    $provider_id
+     * @param  string $date_ymd   (Y-m-d)
+     * @param  string $start_time (H:i:s)
+     * @param  string $end_time   (H:i:s)
      * @return bool
      *
      * @throws \Exception
@@ -115,6 +116,7 @@ class ScheduleBusinessRules
     }
 
     // apenas para custom_schedules
+
     public static function validatePricePeriod($provider_id, $min_price, $max_price, $period_type)
     {
         if ($min_price < 0 || $max_price < 0) {
@@ -126,29 +128,30 @@ class ScheduleBusinessRules
         }
 
         $provider = Provider::find($provider_id);
+
         $min_price_proportional = 0;
         $max_price_proportional = 0;
+        $provider_price_period  = 0;
 
-        $provider_price_period = 0;
         switch ($period_type) {
             case '2': // 2 horas
-                $provider_price_period = $provider->daily_price_2h;
+                $provider_price_period  = $provider->daily_price_2h;
                 $min_price_proportional = $min_price * 0.30;
                 $max_price_proportional = $max_price * 0.30;
                 break;
             case '4': // 4 horas
-                $provider_price_period = $provider->daily_price_4h;
+                $provider_price_period  = $provider->daily_price_4h;
                 $min_price_proportional = $min_price * 0.55;
                 $max_price_proportional = $max_price * 0.55;
                 break;
             case '6': // 6 horas
-                $provider_price_period = $provider->daily_price_6h;
+                $provider_price_period  = $provider->daily_price_6h;
                 $min_price_proportional = $min_price * 0.85;
                 $max_price_proportional = $max_price * 0.85;
 
                 break;
             case '8': // 8 horas
-                $provider_price_period = $provider->daily_price_8h;
+                $provider_price_period  = $provider->daily_price_8h;
                 $min_price_proportional = $min_price;
                 $max_price_proportional = $max_price;
 
@@ -167,11 +170,11 @@ class ScheduleBusinessRules
     /**
      * Valida se o prestador tem outro agendamento no mesmo dia e horário
      *
-     * @param  int  $provider_id
-     * @param  string  $date_ymd  (Y-m-d)
-     * @param  string  $start_time  (H:i:s)
-     * @param  string  $end_time  (H:i:s)
-     * @param  int|null  $exclude_schedule_id  (id do agendamento a ser excluído da validação, usado para edição de agendamento)
+     * @param  int      $provider_id
+     * @param  string   $date_ymd   (Y-m-d)
+     * @param  string   $start_time (H:i:s)
+     * @param  string   $end_time   (H:i:s)
+     * @param  int|null $exclude_schedule_id  (id do agendamento a ser excluído da validação, usado para edição de agendamento)
      * @return bool
      *
      * @throws \Exception
@@ -204,10 +207,10 @@ class ScheduleBusinessRules
     /**
      * Valida se o prestador tem outro agendamento com o mesmo cliente no mesmo dia e horário
      *
-     * @param  int  $provider_id
-     * @param  string  $date_ymd  (Y-m-d)
-     * @param  string  $start_time  (H:i:s)
-     * @param  string  $end_time  (H:i:s)
+     * @param  int       $provider_id
+     * @param  string    $date_ymd   (Y-m-d)
+     * @param  string    $start_time (H:i:s)
+     * @param  string    $end_time   (H:i:s)
      * @param  int|null  $exclude_schedule_id  (id do agendamento a ser excluído da validação, usado para edição de agendamento)
      * @return bool
      *
@@ -231,11 +234,11 @@ class ScheduleBusinessRules
     /**
      * Valida se o prestador tem outro agendamento com o mesmo cliente no mesmo dia e horário, ignorando o horário
      *
-     * @param  int  $provider_id
-     * @param  string  $date_ymd  (Y-m-d)
-     * @param  string  $start_time  (H:i:s)
-     * @param  string  $end_time  (H:i:s)
-     * @param  int|null  $exclude_schedule_id  (id do agendamento a ser excluído da validação, usado para edição de agendamento)
+     * @param  int      $provider_id
+     * @param  string   $date_ymd   (Y-m-d)
+     * @param  string   $start_time (H:i:s)
+     * @param  string   $end_time   (H:i:s)
+     * @param  int|null $exclude_schedule_id  (id do agendamento a ser excluído da validação, usado para edição de agendamento)
      * @return bool
      *
      * @throws \Exception
@@ -322,10 +325,12 @@ class ScheduleBusinessRules
     public static function getBlockedProviderIdsForClient(int $client_id): Collection
     {
         // Prestadores que bloquearam este cliente (ProviderClientBlock)
+
         $blockedByProvider = ProviderClientBlock::where('client_id', $client_id)
             ->pluck('provider_id');
 
         // Prestadores que este cliente bloqueou (ClientProviderBlock)
+
         $blockedByClient = ClientProviderBlock::where('client_id', $client_id)
             ->pluck('provider_id');
 
@@ -352,8 +357,8 @@ class ScheduleBusinessRules
      *  2. Prestador NÃO tem ProviderBlockedDay com period = 'all' nessa data.
      *     (period = 'morning' ou 'afternoon' = bloqueio parcial → ainda disponível)
      *
-     * @param  string  $date_ymd  Y-m-d
-     * @param  Collection  $providerIds  conjunto de IDs a filtrar
+     * @param  string     $date_ymd  Y-m-d
+     * @param  Collection $providerIds  conjunto de IDs a filtrar
      */
     public static function getAvailableProviderIdsForDate(string $date_ymd, Collection $providerIds): Collection
     {

+ 52 - 0
app/Services/CartItemService.php

@@ -0,0 +1,52 @@
+<?php
+
+namespace App\Services;
+
+use App\Models\CartItem;
+use Illuminate\Database\Eloquent\Collection;
+
+class CartItemService
+{
+    public function getAll(): Collection
+    {
+        return CartItem::query()
+            ->orderBy('created_at', 'desc')
+            ->get();
+    }
+
+    public function findById(int $id): ?CartItem
+    {
+        return CartItem::find($id);
+    }
+
+    public function create(array $data): CartItem
+    {
+        return CartItem::create($data);
+    }
+
+    public function update(int $id, array $data): ?CartItem
+    {
+        $model = $this->findById($id);
+
+        if (!$model) {
+            return null;
+        }
+
+        $model->update($data);
+
+        return $model->fresh();
+    }
+
+    public function delete(int $id): bool
+    {
+        $model = $this->findById($id);
+
+        if (!$model) {
+            return false;
+        }
+
+        return $model->delete();
+    }
+
+    // Add custom business logic methods here
+}

+ 149 - 0
app/Services/CartService.php

@@ -0,0 +1,149 @@
+<?php
+
+namespace App\Services;
+
+use App\Models\Cart;
+use App\Models\Client;
+use Illuminate\Support\Arr;
+use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Support\Facades\DB;
+
+class CartService
+{
+    public function getAll(): Collection
+    {
+        return Cart::query()
+            ->with(['items.schedule'])
+            ->orderBy('created_at', 'desc')
+            ->get();
+    }
+
+    public function findById(int $id): ?Cart
+    {
+        return Cart::query()
+            ->with(['items.schedule'])
+            ->find($id);
+    }
+
+    public function create(array $data): Cart
+    {
+        return Cart::create($data);
+    }
+
+    public function update(int $id, array $data): ?Cart
+    {
+        $model = $this->findById($id);
+
+        if (! $model) {
+            return null;
+        }
+
+        $model->update($data);
+
+        return $model->fresh();
+    }
+
+    public function delete(int $id): bool
+    {
+        $model = $this->findById($id);
+
+        if (! $model) {
+            return false;
+        }
+
+        return $model->delete();
+    }
+
+    //
+
+    public function getByUserId(int $userId): Collection
+    {
+        return Cart::query()
+            ->with(['items.schedule.client.user', 'items.schedule.provider.user', 'items.schedule.address'])
+            ->whereHas('client', fn ($query) => $query->where('user_id', $userId))
+            ->orderBy('created_at', 'desc')
+            ->get();
+    }
+
+    public function createForUser(array $data, int $userId): Cart
+    {
+        $client = Client::query()
+            ->where('user_id', $userId)
+            ->firstOrFail();
+
+        $data['client_id'] = $client->id;
+
+        return $this->hasSchedulePayload($data)
+            ? $this->createWithSchedules($data)
+            : $this->create($data);
+    }
+
+    public function createWithSchedules(array $data): Cart
+    {
+        return DB::transaction(function () use ($data) {
+            $items = $this->scheduleItems($data);
+
+            $schedules = app(ScheduleService::class)->createSingleOrMultiple(
+                baseData:  $this->baseScheduleData($data),
+                schedules: $items,
+            );
+
+            $cart = Cart::create([
+                'client_id' => $data['client_id'],
+            ]);
+
+            foreach ($schedules as $schedule) {
+                $cart->items()->create([
+                    'schedule_id' => $schedule->id,
+                ]);
+            }
+
+            return $cart->fresh(['items.schedule.client.user', 'items.schedule.provider.user', 'items.schedule.address']);
+        });
+    }
+
+    //
+
+    private function baseScheduleData(array $data): array
+    {
+        return Arr::except($data, [
+            'schedules',
+            'dates',
+            'date',
+            'period_type',
+            'start_time',
+            'end_time',
+            'total_amount',
+            'offers_meal',
+        ]);
+    }
+
+    private function hasSchedulePayload(array $data): bool
+    {
+        return ! empty($data['schedules'])
+            || ! empty($data['dates'])
+            || ! empty($data['date']);
+    }
+
+    private function scheduleItems(array $data): array
+    {
+        if (! empty($data['schedules'])) {
+            return $data['schedules'];
+        }
+
+        if (! empty($data['dates'])) {
+            return array_map(
+                fn (string $date) => array_merge(
+                    Arr::only($data, ['period_type', 'start_time', 'end_time', 'total_amount', 'offers_meal']),
+                    ['date' => $date],
+                ),
+                $data['dates'],
+            );
+        }
+
+        return [
+            Arr::only($data, ['date', 'period_type', 'start_time', 'end_time', 'total_amount', 'offers_meal']),
+        ];
+    }
+
+}

+ 137 - 66
app/Services/DashboardService.php

@@ -37,12 +37,13 @@ class DashboardService
         $cliente = Client::with('profileMedia')->where('user_id', $user->id)->first();
 
         $headerBar = [
-            'rating'        => $cliente->average_rating,
+            'rating'         => $cliente->average_rating,
+            'total_services' => $cliente->total_services,
+
             'total_ratings' => Review::where('reviews.origin', 'provider')
                 ->leftJoin('schedules', 'schedules.id', '=', 'reviews.schedule_id')
                 ->where('schedules.client_id', $cliente->id)
                 ->count(),
-            'total_services' => $cliente->total_services,
         ];
 
         $address = Address::where('source', 'client')
@@ -52,11 +53,13 @@ class DashboardService
             ->first();
 
         $summaryInfos = [
-            'name'          => $user->name,
+            'name'    => $user->name,
+            'address' => $address,
+
             'profile_photo' => $cliente->profileMedia?->path
                 ? Storage::temporaryUrl($cliente->profileMedia->path, now()->addMinutes(60))
                 : null,
-            'address'          => $address,
+
             'pending_services' => Schedule::where('client_id', $cliente->id)
                 ->whereIn('status', ['pending', 'paid', 'accepted'])
                 ->whereDate('date', '>=', now()->toDateString())
@@ -85,6 +88,18 @@ class DashboardService
                 'schedules.address_id',
                 'custom_schedules.address_type as custom_address_type',
                 'provider_media.path as provider_photo_path',
+
+                DB::raw("(SELECT ci.cart_id FROM cart_items ci WHERE ci.schedule_id = schedules.id LIMIT 1) as cart_id"),
+
+                DB::raw(
+                    "(SELECT COUNT(*) FROM cart_items ci_count
+                        WHERE ci_count.cart_id = (
+                            SELECT ci.cart_id FROM cart_items ci
+                                WHERE ci.schedule_id = schedules.id
+                                    LIMIT 1
+                        )
+                    ) as cart_items_count"
+                ),
             )
             ->orderBy('schedules.date', 'asc')
             ->limit(5)
@@ -94,6 +109,7 @@ class DashboardService
             $item->provider_photo = $item->provider_photo_path
                 ? Storage::temporaryUrl($item->provider_photo_path, now()->addMinutes(60))
                 : null;
+
             unset($item->provider_photo_path);
         });
 
@@ -109,7 +125,7 @@ class DashboardService
             ->leftJoin('users as provider_user', 'provider_user.id', '=', 'providers.user_id')
             ->leftJoinSub(Address::preferredForProvider(), 'provider_address', fn($join) =>
                 $join->on('provider_address.source_id', '=', 'providers.id')
-                     ->where('provider_address.rn', 1)
+                    ->where('provider_address.rn', 1)
             )
             ->leftJoin('media as provider_media', 'provider_media.id', '=', 'providers.profile_media_id')
             ->select(
@@ -127,6 +143,7 @@ class DashboardService
             $item->provider_photo = $item->provider_photo_path
                 ? Storage::temporaryUrl($item->provider_photo_path, now()->addMinutes(60))
                 : null;
+
             unset($item->provider_photo_path);
         });
 
@@ -135,7 +152,7 @@ class DashboardService
             ->leftJoin('users as provider_user', 'provider_user.id', '=', 'providers.user_id')
             ->leftJoinSub(Address::preferredForProvider(), 'provider_address', fn($join) =>
                 $join->on('provider_address.source_id', '=', 'providers.id')
-                     ->where('provider_address.rn', 1)
+                    ->where('provider_address.rn', 1)
             )
             ->leftJoin('media as provider_media', 'provider_media.id', '=', 'providers.profile_media_id')
             ->select(
@@ -153,10 +170,11 @@ class DashboardService
             $item->provider_photo = $item->provider_photo_path
                 ? Storage::temporaryUrl($item->provider_photo_path, now()->addMinutes(60))
                 : null;
+
             unset($item->provider_photo_path);
         });
 
-        $blockedProviderIds = ScheduleBusinessRules::getBlockedProviderIdsForClient($cliente->id);
+        $blockedProviderIds       = ScheduleBusinessRules::getBlockedProviderIdsForClient($cliente->id);
         $providersWithWorkingDays = ScheduleBusinessRules::getProviderIdsWithWorkingDays();
 
         $clientPrimaryAddress = Address::where('source', 'client')
@@ -165,21 +183,18 @@ class DashboardService
             ->first();
 
         $providersCloseDistanceSelect = $this->distanceSelect(
-            $clientPrimaryAddress?->latitude !== null ? (float) $clientPrimaryAddress->latitude : null,
+            $clientPrimaryAddress?->latitude  !== null ? (float) $clientPrimaryAddress->latitude  : null,
             $clientPrimaryAddress?->longitude !== null ? (float) $clientPrimaryAddress->longitude : null,
         );
 
         $providersClose = Provider::leftJoin('users as provider_user', 'provider_user.id', '=', 'providers.user_id')
-            ->leftJoin(DB::raw("
-        (
-          SELECT DISTINCT ON (source_id)
-            *
-          FROM addresses
-          WHERE source = 'provider'
-          AND deleted_at IS NULL
-          ORDER BY source_id, is_primary DESC
-        ) as provider_address
-      "), 'provider_address.source_id', '=', 'providers.id')
+            ->leftJoin(DB::raw(
+                "(SELECT DISTINCT ON (source_id) * FROM addresses
+                    WHERE source = 'provider'
+                        AND deleted_at IS NULL
+                            ORDER BY source_id, is_primary DESC
+                ) as provider_address"), 'provider_address.source_id', '=', 'providers.id'
+            )
             ->leftJoin('media as provider_media', 'provider_media.id', '=', 'providers.profile_media_id')
             ->whereNotNull('provider_address.id')
             ->where('provider_address.city_id', $clientPrimaryAddress?->city_id)
@@ -199,14 +214,17 @@ class DashboardService
                 'providers.daily_price_6h',
                 'providers.daily_price_4h',
                 'providers.daily_price_2h',
-                DB::raw("(
-          SELECT COUNT(*)
-          FROM reviews
-          LEFT JOIN schedules ON schedules.id = reviews.schedule_id
-          WHERE reviews.origin = 'provider'
-          AND schedules.provider_id = providers.id
-        ) as total_reviews"),
+
+                DB::raw(
+                    "(SELECT COUNT(*) FROM reviews
+                        LEFT JOIN schedules ON schedules.id = reviews.schedule_id
+                            WHERE reviews.origin = 'provider'
+                                AND schedules.provider_id = providers.id
+                    ) as total_reviews"
+                ),
+
                 $providersCloseDistanceSelect,
+
                 'provider_media.path as provider_photo_path',
             )
             ->get();
@@ -215,6 +233,7 @@ class DashboardService
             $item->provider_photo = $item->provider_photo_path
                 ? Storage::temporaryUrl($item->provider_photo_path, now()->addMinutes(60))
                 : null;
+
             unset($item->provider_photo_path);
         });
 
@@ -235,12 +254,28 @@ class DashboardService
                 'schedules.total_amount',
                 'schedules.start_time',
                 'schedules.end_time',
-                DB::raw("CASE
-          WHEN (now() - schedules.created_at) < INTERVAL '1 hour' THEN CONCAT(ROUND(EXTRACT(EPOCH FROM (now() - schedules.created_at)) / 60), 'min')
-          WHEN (now() - schedules.created_at) < INTERVAL '1 day' THEN CONCAT(ROUND(EXTRACT(EPOCH FROM (now() - schedules.created_at)) / 3600), 'h')
-          ELSE CONCAT(ROUND(EXTRACT(EPOCH FROM (now() - schedules.created_at)) / 86400), 'd')
-        END as time_since_request"),
+
+                DB::raw(
+                    "CASE
+                        WHEN (now() - schedules.created_at) < INTERVAL '1 hour' THEN CONCAT(ROUND(EXTRACT(EPOCH FROM (now() - schedules.created_at)) / 60), 'min')
+                        WHEN (now() - schedules.created_at) < INTERVAL '1 day' THEN CONCAT(ROUND(EXTRACT(EPOCH FROM (now() - schedules.created_at)) / 3600), 'h')
+                    ELSE CONCAT(ROUND(EXTRACT(EPOCH FROM (now() - schedules.created_at)) / 86400), 'd')
+                    END as time_since_request"
+                ),
+
                 'provider_media.path as provider_photo_path',
+
+                DB::raw("(SELECT ci.cart_id FROM cart_items ci WHERE ci.schedule_id = schedules.id LIMIT 1) as cart_id"),
+
+                DB::raw(
+                    "(SELECT COUNT(*) FROM cart_items ci_count
+                        WHERE ci_count.cart_id = (
+                            SELECT ci.cart_id FROM cart_items ci
+                                WHERE ci.schedule_id = schedules.id
+                                    LIMIT 1
+                        )
+                    ) as cart_items_count"
+                ),
             )
             ->orderBy('schedules.date', 'asc')
             ->get();
@@ -249,11 +284,12 @@ class DashboardService
             $item->provider_photo = $item->provider_photo_path
                 ? Storage::temporaryUrl($item->provider_photo_path, now()->addMinutes(60))
                 : null;
+
             unset($item->provider_photo_path);
         });
 
         $proposalsDistanceSelect = DistanceService::sqlExpression(
-            $clientPrimaryAddress?->latitude !== null ? (float) $clientPrimaryAddress->latitude : null,
+            $clientPrimaryAddress?->latitude  !== null ? (float) $clientPrimaryAddress->latitude  : null,
             $clientPrimaryAddress?->longitude !== null ? (float) $clientPrimaryAddress->longitude : null,
         );
 
@@ -261,14 +297,15 @@ class DashboardService
             ->leftJoin('schedules', 'schedule_proposals.schedule_id', '=', 'schedules.id')
             ->leftJoin('providers', 'schedule_proposals.provider_id', '=', 'providers.id')
             ->leftJoin('users', 'providers.user_id', '=', 'users.id')
-            ->leftJoin(DB::raw("(
-                SELECT DISTINCT ON (source_id)
-                    *
-                FROM addresses
-                WHERE source = 'provider'
-                AND deleted_at IS NULL
-                ORDER BY source_id, is_primary DESC
-            ) as provider_address"), 'provider_address.source_id', '=', 'providers.id')
+            ->leftJoin(
+                DB::raw(
+                    "(SELECT DISTINCT ON (source_id) * FROM addresses
+                        WHERE source = 'provider'
+                            AND deleted_at IS NULL
+                                ORDER BY source_id, is_primary DESC
+                    ) as provider_address"
+                ), 'provider_address.source_id', '=', 'providers.id'
+            )
             ->leftJoin('media as provider_media', 'provider_media.id', '=', 'providers.profile_media_id')
             ->where('schedules.client_id', $cliente->id)
             ->where('schedules.schedule_type', 'custom')
@@ -280,6 +317,7 @@ class DashboardService
                 'schedule_proposals.id',
 
                 DB::raw("DATE_PART('year', AGE(providers.birth_date)) as idade"),
+
                 'providers.id as provider_id',
                 'schedules.id as schedule_id',
                 'schedules.date',
@@ -292,7 +330,9 @@ class DashboardService
                 'providers.total_services',
 
                 'users.name as provider_name',
+
                 $proposalsDistanceSelect,
+
                 'provider_media.path as provider_photo_path',
             ])
             ->get();
@@ -301,6 +341,7 @@ class DashboardService
             $item->provider_photo = $item->provider_photo_path
                 ? Storage::temporaryUrl($item->provider_photo_path, now()->addMinutes(60))
                 : null;
+
             unset($item->provider_photo_path);
         });
 
@@ -326,13 +367,28 @@ class DashboardService
                 'schedules.code_verified',
                 'schedules.code',
                 'media.path as provider_photo',
-                DB::raw("EXISTS(
-          SELECT 1 FROM reviews
-          WHERE reviews.schedule_id = schedules.id
-            AND reviews.origin = 'client'
-            AND reviews.origin_id = {$cliente->id}
-            AND reviews.deleted_at IS NULL
-        ) as client_reviewed"),
+
+                DB::raw("(SELECT ci.cart_id FROM cart_items ci WHERE ci.schedule_id = schedules.id LIMIT 1) as cart_id"),
+
+                DB::raw(
+                    "(SELECT COUNT(*) FROM cart_items ci_count
+                        WHERE ci_count.cart_id = (
+                            SELECT ci.cart_id FROM cart_items ci
+                                WHERE ci.schedule_id = schedules.id
+                                    LIMIT 1
+                        )
+                    ) as cart_items_count"
+                ),
+
+                DB::raw(
+                    "EXISTS(
+                        SELECT 1 FROM reviews
+                            WHERE reviews.schedule_id = schedules.id
+                                AND reviews.origin = 'client'
+                                    AND reviews.origin_id = {$cliente->id}
+                                        AND reviews.deleted_at IS NULL
+                    ) as client_reviewed"
+                ),
             )
             ->orderBy('schedules.start_time', 'asc')
             ->get()
@@ -416,11 +472,12 @@ class DashboardService
         return [
             'provider_name'       => $schedule->provider_name,
             'provider_birth_date' => $schedule->provider_birth_date,
-            'provider_photo'      => $schedule->provider_photo
+            'offers_meal'         => $schedule->offers_meal,
+            'specialities'        => $specialities,
+
+            'provider_photo' => $schedule->provider_photo
                 ? Storage::temporaryUrl($schedule->provider_photo, now()->addMinutes(60))
                 : null,
-            'offers_meal'  => $schedule->offers_meal,
-            'specialities' => $specialities,
         ];
     }
 
@@ -443,12 +500,13 @@ class DashboardService
         $address = Address::where('source', 'provider')->where('source_id', $provider->id)->with(['city', 'state'])->first();
 
         $summaryInfos = [
-            'name'          => $user->name,
+            'name'             => $user->name,
+            'address'          => $address,
+            'pending_services' => Schedule::where('provider_id', $provider->id)->where('status', 'pending')->count(),
+
             'profile_photo' => $provider->profileMedia?->path
                 ? Storage::temporaryUrl($provider->profileMedia->path, now()->addMinutes(60))
                 : null,
-            'address'          => $address,
-            'pending_services' => Schedule::where('provider_id', $provider->id)->where('status', 'pending')->count(),
         ];
 
         $priceSuggestedAvg = Provider::where('user_id', '!=', $user->id)
@@ -475,7 +533,9 @@ class DashboardService
                 'client_user.name as client_name',
                 'clients.average_rating',
                 'schedules.date',
+
                 DB::raw("TO_CHAR(schedules.date, 'DD/MM/YYYY') as formatted_date"),
+
                 'schedules.start_time',
                 'schedules.end_time',
                 'schedules.total_amount',
@@ -485,26 +545,31 @@ class DashboardService
                 'schedules.status',
                 'custom_schedules.offers_meal',
                 'client_media.path as customer_photo_path',
-                DB::raw("CASE
-          WHEN (now() - schedules.created_at) < INTERVAL '1 day' THEN CONCAT(ROUND(EXTRACT(EPOCH FROM (now() - schedules.created_at)) / 3600), ' hours ago')
-          ELSE CONCAT(ROUND(EXTRACT(EPOCH FROM (now() - schedules.created_at)) / 86400), ' days ago')
-        END as time_since_request"),
+
+                DB::raw(
+                    "CASE
+                        WHEN (now() - schedules.created_at) < INTERVAL '1 day' THEN CONCAT(ROUND(EXTRACT(EPOCH FROM (now() - schedules.created_at)) / 3600), ' hours ago')
+                        ELSE CONCAT(ROUND(EXTRACT(EPOCH FROM (now() - schedules.created_at)) / 86400), ' days ago')
+                    END as time_since_request"
+                ),
             )
             ->orderBy('schedules.date', 'asc')
             ->get();
 
-        $providerLat = $address?->latitude !== null ? (float) $address->latitude : null;
+        $providerLat = $address?->latitude  !== null ? (float) $address->latitude  : null;
         $providerLng = $address?->longitude !== null ? (float) $address->longitude : null;
 
         $solicitations->each(function ($solicitation) use ($providerLat, $providerLng) {
             $solicitation->customer_photo = $solicitation->customer_photo_path
                 ? Storage::temporaryUrl($solicitation->customer_photo_path, now()->addMinutes(60))
                 : null;
+
             unset($solicitation->customer_photo_path);
+
             $solicitation->distance_km = DistanceService::calculate(
                 $providerLat,
                 $providerLng,
-                $solicitation->address?->latitude !== null ? (float) $solicitation->address->latitude : null,
+                $solicitation->address?->latitude  !== null ? (float) $solicitation->address->latitude  : null,
                 $solicitation->address?->longitude !== null ? (float) $solicitation->address->longitude : null,
             );
         });
@@ -533,13 +598,16 @@ class DashboardService
                 'schedules.code',
                 'custom_schedules.offers_meal',
                 'client_media.path as customer_photo_path',
-                DB::raw("EXISTS(
-          SELECT 1 FROM reviews
-          WHERE reviews.schedule_id = schedules.id
-            AND reviews.origin = 'provider'
-            AND reviews.origin_id = {$provider->id}
-            AND reviews.deleted_at IS NULL
-        ) as provider_reviewed"),
+
+                DB::raw(
+                    "EXISTS(
+                        SELECT 1 FROM reviews
+                            WHERE reviews.schedule_id = schedules.id
+                                AND reviews.origin = 'provider'
+                                    AND reviews.origin_id = {$provider->id}
+                                        AND reviews.deleted_at IS NULL
+                    ) as provider_reviewed"
+                ),
             )
             ->orderBy('schedules.start_time', 'asc')
             ->get();
@@ -548,6 +616,7 @@ class DashboardService
             $s->customer_photo = $s->customer_photo_path
                 ? Storage::temporaryUrl($s->customer_photo_path, now()->addMinutes(60))
                 : null;
+
             unset($s->customer_photo_path);
         });
 
@@ -580,11 +649,13 @@ class DashboardService
             $schedule->customer_photo = $schedule->customer_photo_path
                 ? Storage::temporaryUrl($schedule->customer_photo_path, now()->addMinutes(60))
                 : null;
+
             unset($schedule->customer_photo_path);
+
             $schedule->distance_km = DistanceService::calculate(
                 $providerLat,
                 $providerLng,
-                $schedule->address?->latitude !== null ? (float) $schedule->address->latitude : null,
+                $schedule->address?->latitude  !== null ? (float) $schedule->address->latitude  : null,
                 $schedule->address?->longitude !== null ? (float) $schedule->address->longitude : null,
             );
         });

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

@@ -12,6 +12,32 @@ trait FormatsPagarmeData
         return preg_replace('/\D+/', '', (string) $value) ?? '';
     }
 
+    protected function customerDocument(?string $value): string
+    {
+        $document = trim((string) $value);
+
+        if (preg_match('/[A-Za-z]/', $document) === 1) {
+            return $document;
+        }
+
+        return $this->digits($document);
+    }
+
+    protected function customerDocumentType(string $document): string
+    {
+        if (preg_match('/[A-Za-z]/', $document) === 1) {
+            return 'PASSPORT';
+        }
+
+        $length = strlen($this->digits($document));
+
+        return match ($length) {
+            11      => 'CPF',
+            14      => 'CNPJ',
+            default => 'PASSPORT',
+        };
+    }
+
     protected function extractAddressParts(array $data): array
     {
         $addressLine   = trim((string) ($data['address'] ?? ''));

+ 4 - 5
app/Services/Pagarme/Concerns/SendsPagarmeRequests.php

@@ -10,11 +10,10 @@ use Throwable;
 trait SendsPagarmeRequests
 {
     protected function pagarmeRequest(
-        string            $method,
-        string            $path,
-        string            $idempotencyKey,
-        string            $errorMessage,
-        array|PagarmeData $payload,
+        string $method,
+        string $path,
+        string $idempotencyKey,
+        string $errorMessage, array|PagarmeData $payload,
     ): array {
         $payload  = $payload instanceof PagarmeData ? $payload->toArray() : $payload;
         $endpoint = $this->pagarmeUrl($path);

+ 202 - 0
app/Services/Pagarme/PagarmeAnticipationService.php

@@ -0,0 +1,202 @@
+<?php
+
+namespace App\Services\Pagarme;
+
+use App\Data\Pagarme\Anticipation\AnticipationLimitData;
+use App\Data\Pagarme\Anticipation\BulkAnticipationLimitsResponseData;
+use App\Data\Pagarme\Anticipation\BulkAnticipationRequestData;
+use App\Data\Pagarme\Anticipation\BulkAnticipationResponseData;
+use App\Data\Pagarme\Order\OrderRequestData;
+use App\Models\Payment;
+use App\Services\Pagarme\Concerns\SendsPagarmeRequests;
+use Illuminate\Support\Facades\Http;
+use Illuminate\Support\Facades\Log;
+use Throwable;
+
+class PagarmeAnticipationService
+{
+    use SendsPagarmeRequests;
+
+    public function createBulkAnticipation(
+        Payment $payment,
+        ?BulkAnticipationLimitsResponseData $limits = null,
+    ): ?BulkAnticipationResponseData
+    {
+        $provider = $payment->provider()->first();
+
+        if (! $provider) {
+            return null;
+        }
+
+        $recipientId = $provider->recipient_id;
+
+        if (empty($recipientId)) {
+            return null;
+        }
+
+        $providerSplit = $payment->splits()
+            ->where('provider_id', $payment->provider_id)
+            ->first();
+
+        if (! $providerSplit) {
+            return null;
+        }
+
+        $requestedAmount = OrderRequestData::amountInCents((float) $providerSplit->gross_amount);
+
+        if ($requestedAmount <= 0) {
+            return null;
+        }
+
+        $paymentDate = $this->getAnticipationPaymentDate();
+
+        $limits ??= $this->fetchAnticipationLimits($recipientId, $paymentDate);
+
+        if ($limits && $limits->maximum->amount <= 0) {
+            Log::channel('pagarme')->warning('Antecipacao ignorada: recebedor sem valor disponivel para antecipar.', [
+                'payment_id'       => $payment->id,
+                'provider_id'      => $payment->provider_id,
+                'recipient_id'     => $recipientId,
+                'requested_amount' => $requestedAmount,
+                'minimum_amount'   => $limits->minimum->amount,
+                'maximum_amount'   => $limits->maximum->amount,
+            ]);
+
+            return null;
+        }
+
+        if ($limits && $limits->minimum->amount > 0 && $requestedAmount < $limits->minimum->amount) {
+            Log::channel('pagarme')->warning('Antecipacao ignorada: valor abaixo do minimo do recebedor.', [
+                'payment_id'       => $payment->id,
+                'provider_id'      => $payment->provider_id,
+                'recipient_id'     => $recipientId,
+                'requested_amount' => $requestedAmount,
+                'minimum_amount'   => $limits->minimum->amount,
+                'maximum_amount'   => $limits->maximum->amount,
+            ]);
+
+            return null;
+        }
+
+        if ($limits && $limits->maximum->amount > 0 && $requestedAmount > $limits->maximum->amount) {
+            Log::channel('pagarme')->warning('Antecipacao ignorada: valor acima do maximo disponivel do recebedor.', [
+                'payment_id'       => $payment->id,
+                'provider_id'      => $payment->provider_id,
+                'recipient_id'     => $recipientId,
+                'requested_amount' => $requestedAmount,
+                'minimum_amount'   => $limits->minimum->amount,
+                'maximum_amount'   => $limits->maximum->amount,
+            ]);
+
+            return null;
+        }
+
+        try {
+            $response = BulkAnticipationResponseData::fromArray($this->pagarmeRequest(
+                method: 'POST',
+                path:   "/recipients/{$recipientId}/bulk_anticipations",
+
+                payload: new BulkAnticipationRequestData(
+                    paymentDate:       $paymentDate,
+                    timeframe:         'start',
+                    requestedAmount:   $requestedAmount,
+                    automaticTransfer: false,
+                ),
+
+                idempotencyKey: "bulk-ant-{$payment->id}-{$payment->provider_id}",
+                errorMessage:   'Erro ao criar antecipacao no Pagar.me.',
+            ));
+
+            $response->requireId();
+
+            return $response;
+        } catch (Throwable $e) {
+            Log::channel('pagarme')->warning('Falha ao criar antecipacao no Pagar.me.', [
+                'payment_id'       => $payment->id,
+                'provider_id'      => $payment->provider_id,
+                'recipient_id'     => $recipientId,
+                'requested_amount' => $requestedAmount,
+                'minimum_amount'   => $limits?->minimum->amount,
+                'maximum_amount'   => $limits?->maximum->amount,
+                'error'            => $e->getMessage(),
+            ]);
+
+            return null;
+        }
+    }
+
+    public function fetchAnticipationLimitsForPayment(Payment $payment): ?BulkAnticipationLimitsResponseData
+    {
+        $provider = $payment->provider()->first();
+
+        if (! $provider || empty($provider->recipient_id)) {
+            return null;
+        }
+
+        return $this->fetchAnticipationLimits(
+            recipientId: $provider->recipient_id,
+            paymentDate: $this->getAnticipationPaymentDate(),
+        );
+    }
+
+    private function fetchAnticipationLimits(string $recipientId, string $paymentDate): ?BulkAnticipationLimitsResponseData
+    {
+        try {
+            $secretKey = config('services.pagarme.secret_key');
+
+            if (empty($secretKey)) {
+                return null;
+            }
+
+            $endpoint = $this->pagarmeUrl("/recipients/{$recipientId}/bulk_anticipations/limits");
+
+            $response = Http::withBasicAuth($secretKey, '')
+                ->withHeaders([
+                    'Content-Type' => 'application/json',
+                    'Accept'       => 'application/json',
+                ])
+                ->get($endpoint, [
+                    'payment_date' => $paymentDate,
+                    'timeframe'    => 'start',
+                ])
+                ->throw();
+
+            $body = $response->json() ?? [];
+            $limits = BulkAnticipationLimitsResponseData::fromArray($body);
+
+            Log::channel('pagarme')->info('Limites de antecipacao consultados no Pagar.me.', [
+                'recipient_id'   => $recipientId,
+                'payment_date'   => $paymentDate,
+                'minimum_amount' => $limits->minimum->amount,
+                'maximum_amount' => $limits->maximum->amount,
+            ]);
+
+            return $limits;
+        } catch (Throwable $e) {
+            Log::channel('pagarme')->warning('Falha ao consultar limites de antecipacao.', [
+                'recipient_id' => $recipientId,
+                'payment_date' => $paymentDate,
+                'error'        => $e->getMessage(),
+            ]);
+
+            return null;
+        }
+    }
+
+    private function getAnticipationPaymentDate(): string
+    {
+        $now = now()->timezone('America/Sao_Paulo');
+
+        if ($now->hour < 11) {
+            return $now->toISOString();
+        }
+
+        $nextBusinessDay = $now->copy()->addDay();
+
+        while ($nextBusinessDay->isWeekend()) {
+            $nextBusinessDay->addDay();
+        }
+
+        return $nextBusinessDay->startOfDay()->toISOString();
+    }
+}

+ 4 - 4
app/Services/Pagarme/PagarmeCardService.php

@@ -2,9 +2,9 @@
 
 namespace App\Services\Pagarme;
 
-use App\Data\Pagarme\Request\CardRequestData\CardBillingAddressData;
-use App\Data\Pagarme\Request\CardRequestData\CardRequestData;
-use App\Data\Pagarme\Response\CardResponseData;
+use App\Data\Pagarme\Card\CardRequestData;
+use App\Data\Pagarme\Card\CardResponseData;
+use App\Data\Pagarme\Card\Parts\Request\BillingAddressData;
 use App\Models\Address;
 use App\Models\ClientPaymentMethod;
 use App\Services\Pagarme\Concerns\SendsPagarmeRequests;
@@ -38,7 +38,7 @@ class PagarmeCardService
                 token: $paymentMethod->token,
                 label: $paymentMethod->card_name,
 
-                billingAddress: CardBillingAddressData::fromAddress(
+                billingAddress: BillingAddressData::fromAddress(
                     Address::query()
                         ->with(['city.state', 'state'])
                         ->where('source', 'client')

+ 16 - 17
app/Services/Pagarme/PagarmeCustomerService.php

@@ -2,11 +2,11 @@
 
 namespace App\Services\Pagarme;
 
-use App\Data\Pagarme\Request\CustomerRequestData\CustomerAddressRequestData;
-use App\Data\Pagarme\Request\CustomerRequestData\CustomerPhonesRequestData\CustomerPhoneData;
-use App\Data\Pagarme\Request\CustomerRequestData\CustomerPhonesRequestData\CustomerPhonesRequestData;
-use App\Data\Pagarme\Request\CustomerRequestData\CustomerRequestData;
-use App\Data\Pagarme\Response\CustomerResponseData\CustomerResponseData;
+use App\Data\Pagarme\Customer\CustomerRequestData;
+use App\Data\Pagarme\Customer\CustomerResponseData;
+use App\Data\Pagarme\Customer\Parts\Request\AddressData;
+use App\Data\Pagarme\Customer\Parts\Request\PhoneData;
+use App\Data\Pagarme\Customer\Parts\Request\PhonesData;
 use App\Models\Client;
 use App\Services\Pagarme\Concerns\FormatsPagarmeData;
 use App\Services\Pagarme\Concerns\SendsPagarmeRequests;
@@ -24,13 +24,12 @@ class PagarmeCustomerService
 
         $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  = $client->ensureGatewayCode();
 
-        $document = $this->digits($client->document ?? $data['document'] ?? null);
-
-        $documentLen = strlen($document);
+        $document     = $this->customerDocument($client->document ?? $data['document'] ?? null);
+        $documentType = $this->customerDocumentType($document);
 
         $address = $this->buildAddressData($data);
         $phones  = $this->buildPhones($client->user?->phone ?? $data['phone'] ?? null);
@@ -39,8 +38,8 @@ class PagarmeCustomerService
             name:         $name,
             email:        (string) $email,
             document:     $document,
-            type:         $documentLen === 14 ? 'company' : 'individual',
-            documentType: $documentLen === 14 ? 'CNPJ' : 'CPF',
+            type:         $documentType === 'CNPJ' ? 'company' : 'individual',
+            documentType: $documentType,
             code:         $code,
             address:      $address,
             phones:       $phones,
@@ -66,7 +65,7 @@ class PagarmeCustomerService
 
     //
 
-    private function buildAddressData(array $data): CustomerAddressRequestData
+    private function buildAddressData(array $data): AddressData
     {
         $line1Parts = array_filter([
             (string) ($data['number'] ?? '0'),
@@ -74,7 +73,7 @@ class PagarmeCustomerService
             (string) ($data['district'] ?? ''),
         ], static fn ($value) => $value !== '');
 
-        return new CustomerAddressRequestData(
+        return new AddressData(
             line1:   implode(', ', $line1Parts),
             line2:   (string) ($data['complement'] ?? $data['instructions'] ?? ''),
             zipCode: $this->digits($data['zip_code'] ?? null),
@@ -84,12 +83,12 @@ class PagarmeCustomerService
         );
     }
 
-    private function buildPhones(?string $phone): CustomerPhonesRequestData
+    private function buildPhones(?string $phone): PhonesData
     {
         $digits = $this->digits($phone);
 
         if ($digits === '') {
-            return new CustomerPhonesRequestData;
+            return new PhonesData;
         }
 
         $areaCode = substr($digits, 0, 2);
@@ -100,8 +99,8 @@ class PagarmeCustomerService
             $number   = $digits;
         }
 
-        return new CustomerPhonesRequestData(
-            mobilePhone: new CustomerPhoneData(
+        return new PhonesData(
+            mobilePhone: new PhoneData(
                 countryCode: '55',
                 areaCode:    $areaCode,
                 number:      $number,

+ 184 - 34
app/Services/Pagarme/PagarmePaymentService.php

@@ -2,28 +2,30 @@
 
 namespace App\Services\Pagarme;
 
-use App\Data\Pagarme\Request\CustomerRequestData\CustomerAddressRequestData;
-use App\Data\Pagarme\Request\CustomerRequestData\CustomerPhonesRequestData\CustomerPhoneData;
-use App\Data\Pagarme\Request\CustomerRequestData\CustomerPhonesRequestData\CustomerPhonesRequestData;
-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\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\Data\Pagarme\Customer\CustomerRequestData;
+use App\Data\Pagarme\Customer\Parts\Request\AddressData;
+use App\Data\Pagarme\Customer\Parts\Request\PhoneData;
+use App\Data\Pagarme\Customer\Parts\Request\PhonesData;
+use App\Data\Pagarme\Order\OrderRequestData;
+use App\Data\Pagarme\Order\OrderResponseData;
+use App\Data\Pagarme\Order\Parts\Request\CreditCardData;
+use App\Data\Pagarme\Order\Parts\Request\ItemData;
+use App\Data\Pagarme\Order\Parts\Request\PaymentData;
+use App\Data\Pagarme\Order\Parts\Request\PixAdditionalInformationData;
+use App\Data\Pagarme\Order\Parts\Request\PixData;
+use App\Data\Pagarme\Order\Parts\Request\SplitData;
+use App\Data\Pagarme\Order\Parts\Request\SplitOptionsData;
 use App\Enums\PaymentSplitStatusEnum;
 use App\Enums\PaymentStatusEnum;
 use App\Models\Address;
+use App\Models\Cart;
 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\Facades\Log;
 use Illuminate\Support\Str;
 
 class PagarmePaymentService
@@ -31,6 +33,60 @@ class PagarmePaymentService
     use FormatsPagarmeData;
     use SendsPagarmeRequests;
 
+    public function calculatePaymentAmounts(float $serviceAmount, string $paymentMethod, ?Schedule $schedule = null): array
+    {
+        if ($serviceAmount <= 0) {
+            throw new \InvalidArgumentException('Valor do servico precisa ser maior que zero.');
+        }
+
+        if (! in_array($paymentMethod, ['credit_card', 'pix'], true)) {
+            throw new \InvalidArgumentException('Forma de pagamento invalida.');
+        }
+
+        $platformFeeRate = $this->platformFeeRate($paymentMethod, $schedule);
+
+        $platformFee = round($serviceAmount * $platformFeeRate, 2);
+        $grossAmount = round($serviceAmount + $platformFee, 2);
+
+        if ($platformFee > 0 && empty(config('services.pagarme.platform_recipient_id'))) {
+            throw new \InvalidArgumentException('PAGARME_PLATFORM_RECIPIENT_ID precisa estar configurado para receber a taxa da plataforma no split.');
+        }
+
+        return [
+            'service_amount'      => round($serviceAmount, 2),
+            'platform_fee_amount' => $platformFee,
+            'gross_amount'        => $grossAmount,
+        ];
+    }
+
+    public function platformFeeRates(): array
+    {
+        return [
+            'pix'                  => (float) config('services.pagarme.platform_pix_fee_rate'),
+            'credit_card'          => (float) config('services.pagarme.platform_credit_card_fee_rate'),
+            'cart_min_3_schedules' => (float) config('services.pagarme.platform_cart_min_3_schedules_fee_rate'),
+        ];
+    }
+
+    private function platformFeeRate(string $paymentMethod, ?Schedule $schedule = null): float
+    {
+        if ($schedule && $this->scheduleBelongsToCartWithAtLeastThreeItems($schedule)) {
+            return (float) config('services.pagarme.platform_cart_min_3_schedules_fee_rate');
+        }
+
+        return $paymentMethod === 'credit_card'
+            ? (float) config('services.pagarme.platform_credit_card_fee_rate')
+            : (float) config('services.pagarme.platform_pix_fee_rate');
+    }
+
+    private function scheduleBelongsToCartWithAtLeastThreeItems(Schedule $schedule): bool
+    {
+        return Cart::query()
+            ->whereHas('items', fn ($query) => $query->where('schedule_id', $schedule->id))
+            ->whereHas('items', null, '>=', 3)
+            ->exists();
+    }
+
     public function processPayment(
         Payment  $payment,
         Schedule $schedule,
@@ -51,27 +107,37 @@ class PagarmePaymentService
         $orderOptions = array_merge(['split' => $split], $pixOptions);
 
         if ($paymentMethod === 'credit_card') {
-            $creditCard = new OrderCreditCardData(
+            $creditCard = new CreditCardData(
                 cardId:              $cardId,
-                installments:        1,
+                installments:        $payment->installments,
                 statementDescriptor: Str::limit((string) config('app.name', 'SOFTPAR'), 13, ''),
                 operationType:       'auth_and_capture',
             );
 
-            return $this->createOrderWithCreditCard(
+            $result = $this->createOrderWithCreditCard(
                 payment:    $payment,
                 items:      $items,
                 customer:   $customer,
                 creditCard: $creditCard,
                 options:    $orderOptions,
             );
+
+            $this->logCreditCardOrderResult($payment, $result, 'create_order');
+
+            $orderStatus = OrderResponseData::fromArray($result)->paymentStatus();
+
+            if (! in_array($orderStatus, [PaymentStatusEnum::PAID, PaymentStatusEnum::AUTHORIZED], true)) {
+                return $result;
+            }
+
+            return $result;
         }
 
-        $pixData = new OrderPixData(
+        $pixData = new PixData(
             expiresIn: 1800,
 
             additionalInformation: [
-                new OrderPixAdditionalInformationData(
+                new PixAdditionalInformationData(
                     name:  'Agendamento',
                     value: (string) $schedule->id,
                 ),
@@ -91,7 +157,7 @@ class PagarmePaymentService
         Payment             $payment,
         array               $items,
         CustomerRequestData $customer,
-        OrderCreditCardData $creditCard,
+        CreditCardData      $creditCard,
         array               $options = []
     ): array {
         return $this->createOrder(
@@ -112,7 +178,7 @@ class PagarmePaymentService
         Payment             $payment,
         array               $items,
         CustomerRequestData $customer,
-        OrderPixData        $pix,
+        PixData             $pix,
         array               $options = []
     ): array {
         return $this->createOrder(
@@ -133,7 +199,7 @@ class PagarmePaymentService
         Payment             $payment,
         array               $items,
         CustomerRequestData $customer,
-        OrderPaymentData    $paymentMethod,
+        PaymentData         $paymentMethod,
         array               $options = []
     ): array {
         $metadata = array_merge([
@@ -175,17 +241,25 @@ class PagarmePaymentService
     {
         $order = OrderResponseData::fromArray($orderResponse);
 
-        $newStatus      = $order->paymentStatus();
+        $newStatus = $order->paymentStatus();
+
         $failureCode    = null;
         $failureMessage = null;
 
         if ($newStatus === PaymentStatusEnum::FAILED) {
             $failureCode    = $order->failureCode();
             $failureMessage = $order->failureMessage();
+
+            $this->logCreditCardOrderResult($payment, $orderResponse, 'webhook_failed');
         }
 
         $gatewayFeeCents = $order->lastTransaction()?->cost ?? 0;
-        $gatewayFee      = $gatewayFeeCents > 0 ? round($gatewayFeeCents / 100, 2) : 0;
+
+        $gatewayFee = $gatewayFeeCents > 0 ? round($gatewayFeeCents / 100, 2) : 0;
+
+        $gatewayPayload = $newStatus === PaymentStatusEnum::FAILED
+            ? $this->normalizeFailedGatewayPayload($orderResponse, $failureCode, $failureMessage)
+            : $orderResponse;
 
         $payment->forceFill([
             'gateway_provider'            => 'pagarme',
@@ -196,7 +270,7 @@ class PagarmePaymentService
             'status'                      => $newStatus,
             'paid_at'                     => $order->paidAt(),
             'authorized_at'               => $order->authorizedAt(),
-            'gateway_payload'             => $orderResponse,
+            'gateway_payload'             => $gatewayPayload,
             'gateway_fee_amount'          => $gatewayFee,
             'failure_code'                => $failureCode,
             'failure_message'             => $failureMessage,
@@ -239,7 +313,8 @@ class PagarmePaymentService
             throw new \InvalidArgumentException('Endereco do agendamento nao encontrado para criar pedido no Pagar.me.');
         }
 
-        $document = $this->digits($client->document);
+        $document     = $this->customerDocument($client->document);
+        $documentType = $this->customerDocumentType($document);
 
         $phone = $this->buildPhonePayload($user->phone)
             ?: $this->buildPhonePayload($options['phone'] ?? null);
@@ -267,7 +342,7 @@ class PagarmePaymentService
             }
         }
 
-        $customerAddress = new CustomerAddressRequestData(
+        $customerAddress = new AddressData(
             line1:   $line1,
             line2:   $address->complement ?: $address->instructions,
             zipCode: $zipCode,
@@ -279,8 +354,8 @@ class PagarmePaymentService
         $customerPhones = null;
 
         if ($phone) {
-            $customerPhones = new CustomerPhonesRequestData(
-                mobilePhone: new CustomerPhoneData(
+            $customerPhones = new PhonesData(
+                mobilePhone: new PhoneData(
                     countryCode: $phone['country_code'],
                     areaCode:    $phone['area_code'],
                     number:      $phone['number'],
@@ -292,8 +367,8 @@ class PagarmePaymentService
             name:         $user->name,
             email:        $user->email,
             document:     $document,
-            type:         strlen($document) === 14 ? 'company' : 'individual',
-            documentType: strlen($document) === 14 ? 'CNPJ' : 'CPF',
+            type:         $documentType === 'CNPJ' ? 'company' : 'individual',
+            documentType: $documentType,
             code:         $client->ensureGatewayCode(),
             address:      $customerAddress,
             phones:       $customerPhones,
@@ -305,7 +380,7 @@ class PagarmePaymentService
         $description = $schedule->customSchedule?->serviceType?->description
             ?? "Servico {$schedule->id}";
 
-        return [new OrderItemData(
+        return [new ItemData(
             code:        "schedule-{$schedule->id}",
             amount:      OrderRequestData::amountInCents($grossAmount),
             quantity:    1,
@@ -313,6 +388,76 @@ class PagarmePaymentService
         )];
     }
 
+    private function logCreditCardOrderResult(Payment $payment, array $orderResponse, string $source): void
+    {
+        $order       = OrderResponseData::fromArray($orderResponse);
+        $charge      = $order->firstCharge();
+        $transaction = $order->lastTransaction();
+        $failureCode = $order->failureCode();
+
+        Log::channel('pagarme')->info('Pagar.me credit card order result', [
+            'source'             => $source,
+            'payment_id'         => $payment->id,
+            'provider_id'        => $payment->provider_id,
+            'order_id'           => $order->id,
+            'order_status'       => $order->status,
+            'charge_id'          => $charge?->id,
+            'charge_status'      => $charge?->status,
+            'transaction_id'     => $transaction?->id,
+            'transaction_status' => $transaction?->status,
+            'failure_code'       => $failureCode,
+            'failure_message'    => $order->failureMessage(),
+            'acquirer_message'   => $transaction?->acquirerMessage,
+
+            'gateway_response' => $this->normalizeGatewayResponseForFailure(
+                $transaction?->gatewayResponse ?? [],
+                $failureCode,
+            ),
+        ]);
+    }
+
+    private function normalizeFailedGatewayPayload(array $payload, ?string $failureCode, ?string $failureMessage): array
+    {
+        $payload['failure_code']    = $failureCode;
+        $payload['failure_message'] = $failureMessage;
+
+        if (isset($payload['charges'][0]['last_transaction']['gateway_response'])
+            && is_array($payload['charges'][0]['last_transaction']['gateway_response'])) {
+            $payload['charges'][0]['last_transaction']['gateway_response'] = $this->normalizeGatewayResponseForFailure(
+                $payload['charges'][0]['last_transaction']['gateway_response'],
+                $failureCode,
+            );
+        }
+
+        return $payload;
+    }
+
+    private function normalizeGatewayResponseForFailure(array $gatewayResponse, ?string $failureCode): array
+    {
+        $code = $gatewayResponse['code'] ?? null;
+
+        if (! $failureCode || ! $this->isMisleadingGatewayCode($code)) {
+            return $gatewayResponse;
+        }
+
+        $gatewayResponse['raw_code'] = $code;
+        $gatewayResponse['code']     = $failureCode;
+
+        return $gatewayResponse;
+    }
+
+    private function isMisleadingGatewayCode(mixed $code): bool
+    {
+        if ($code === null || $code === '') {
+            return false;
+        }
+
+        $code = mb_strtolower((string) $code);
+
+        return preg_match('/^[1-2]\d{2}$/', $code) === 1
+            || in_array($code, ['00', '0', 'approved', 'success'], true);
+    }
+
     private function buildPhonePayload(?string $phone): ?array
     {
         $digits = $this->digits($phone);
@@ -332,6 +477,11 @@ class PagarmePaymentService
         ];
     }
 
+    private function roundMoneyUp(float $amount): float
+    {
+        return ceil($amount * 100) / 100;
+    }
+
     private function buildSplit(Payment $payment, array $options): array
     {
         $transfers = PaymentSplit::query()
@@ -349,19 +499,19 @@ class PagarmePaymentService
         $orderAmountCents = OrderRequestData::amountInCents((float) $payment->gross_amount);
 
         $providerTotalCents = array_sum(array_map(
-            static fn (OrderSplitData $s) => $s->amount,
+            static fn (SplitData $s) => $s->amount,
             $split,
         ));
 
         $platformAmountCents = $orderAmountCents - $providerTotalCents;
 
         if ($platformAmountCents > 0) {
-            $split[] = new OrderSplitData(
+            $split[] = new SplitData(
                 amount:      $platformAmountCents,
                 recipientId: $platformRecipientId,
                 type:        'flat',
 
-                options: new OrderSplitOptionsData(
+                options: new SplitOptionsData(
                     chargeProcessingFee: true,
                     chargeRemainderFee:  true,
                     liable:              true,

+ 26 - 22
app/Services/Pagarme/PagarmeRecipientService.php

@@ -2,16 +2,16 @@
 
 namespace App\Services\Pagarme;
 
-use App\Data\Pagarme\Request\BankAccountUpdateRequestData;
-use App\Data\Pagarme\Request\RecipientRequestData\RecipientAutomaticAnticipationSettingsData;
-use App\Data\Pagarme\Request\RecipientRequestData\RecipientBankAccountData;
-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\Data\Pagarme\Recipient\BankAccountUpdateRequestData;
+use App\Data\Pagarme\Recipient\Parts\Request\AddressData;
+use App\Data\Pagarme\Recipient\Parts\Request\AutomaticAnticipationSettingsData;
+use App\Data\Pagarme\Recipient\Parts\Request\BankAccountData;
+use App\Data\Pagarme\Recipient\Parts\Request\PhoneData;
+use App\Data\Pagarme\Recipient\Parts\Request\PhoneNumbersData;
+use App\Data\Pagarme\Recipient\Parts\Request\RegisterInformationData;
+use App\Data\Pagarme\Recipient\Parts\Request\TransferSettingsData;
+use App\Data\Pagarme\Recipient\RecipientRequestData;
+use App\Data\Pagarme\Recipient\RecipientResponseData;
 use App\Models\Provider;
 use App\Services\Pagarme\Concerns\FormatsPagarmeData;
 use App\Services\Pagarme\Concerns\SendsPagarmeRequests;
@@ -33,7 +33,7 @@ class PagarmeRecipientService
 
         $addressParts = $this->extractAddressParts($data);
 
-        $registerInformation = new RecipientRegisterInformationData(
+        $registerInformation = new RegisterInformationData(
             name:                   $data['recipient_name'],
             email:                  $data['recipient_email'],
             document:               $this->digits($data['recipient_document'] ?? null),
@@ -42,11 +42,11 @@ class PagarmeRecipientService
             monthlyIncome:          isset($data['monthly_income']) ? (int) $data['monthly_income'] : 1000,
             professionalOccupation: $data['professional_occupation'] ?? 'autonomo',
 
-            phoneNumbers: new RecipientPhoneNumbersData(
+            phoneNumbers: new PhoneNumbersData(
                 $this->buildRecipientPhone($data['phone'] ?? null),
             ),
 
-            address: new RecipientAddressData(
+            address: new AddressData(
                 street:         $addressParts['street'],
                 complementary:  $addressParts['complementary'],
                 streetNumber:   $addressParts['street_number'],
@@ -68,14 +68,16 @@ class PagarmeRecipientService
             registerInformation: $registerInformation,
             defaultBankAccount:  $defaultBankAccount,
 
-            transferSettings: new RecipientTransferSettingsData(
+            transferSettings: new TransferSettingsData(
                 transferEnabled:  false,
                 transferInterval: 'Daily',
                 transferDay:      0,
             ),
 
-            automaticAnticipationSettings: new RecipientAutomaticAnticipationSettingsData(
-                enabled: false,
+            automaticAnticipationSettings: new AutomaticAnticipationSettingsData(
+                enabled: true,
+                type:    '1025', // isso significa que vai ser feita de acordo com o delay (dias)
+                delay:   1,
             ),
         );
 
@@ -111,7 +113,9 @@ class PagarmeRecipientService
             ],
 
             'recipient_automatic_anticipation_settings' => [
-                'enabled' => false,
+                'enabled' => true,
+                'type'    => 'full',
+                'delay'   => 1,
             ],
 
             'recipient_metadata' => $metadata,
@@ -153,9 +157,9 @@ class PagarmeRecipientService
 
     //
 
-    private function buildRecipientBankAccount(array $data): RecipientBankAccountData
+    private function buildRecipientBankAccount(array $data): BankAccountData
     {
-        return new RecipientBankAccountData(
+        return new BankAccountData(
             holderName:        $this->normalizeHolderName($data['holder_name']),
             holderType:        $data['holder_type'],
             holderDocument:    $this->digits($data['holder_document']),
@@ -168,12 +172,12 @@ class PagarmeRecipientService
         );
     }
 
-    private function buildRecipientPhone(?string $phone): RecipientPhoneData
+    private function buildRecipientPhone(?string $phone): PhoneData
     {
         $digits = $this->digits($phone);
 
         if (strlen($digits) < 10) {
-            return new RecipientPhoneData(
+            return new PhoneData(
                 ddd:    '11',
                 number: '999999999',
                 type:   'mobile',
@@ -184,7 +188,7 @@ class PagarmeRecipientService
             $digits = substr($digits, 2);
         }
 
-        return new RecipientPhoneData(
+        return new PhoneData(
             ddd:    substr($digits, 0, 2),
             number: substr($digits, 2),
             type:   'mobile',

+ 2 - 2
app/Services/Pagarme/PagarmeTransferService.php

@@ -2,8 +2,8 @@
 
 namespace App\Services\Pagarme;
 
-use App\Data\Pagarme\Request\TransferRequestData;
-use App\Data\Pagarme\Response\TransferResponseData;
+use App\Data\Pagarme\Transfer\TransferRequestData;
+use App\Data\Pagarme\Transfer\TransferResponseData;
 use App\Models\Provider;
 use App\Models\ProviderWithdrawal;
 use App\Services\Pagarme\Concerns\SendsPagarmeRequests;

+ 175 - 27
app/Services/PaymentService.php

@@ -2,15 +2,19 @@
 
 namespace App\Services;
 
+use App\Enums\CartStatusEnum;
 use App\Enums\PaymentSplitStatusEnum;
 use App\Enums\PaymentStatusEnum;
+use App\Models\Cart;
 use App\Models\ClientPaymentMethod;
 use App\Models\Payment;
 use App\Models\PaymentSplit;
 use App\Models\Schedule;
 use App\Services\Pagarme\PagarmePaymentService;
 use Carbon\Carbon;
+use Illuminate\Auth\Access\AuthorizationException;
 use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Support\Collection as SupportCollection;
 use Illuminate\Support\Str;
 
 class PaymentService
@@ -22,7 +26,7 @@ class PaymentService
     public function getAll(): Collection
     {
         return Payment::query()
-            ->with(['client.user', 'provider.user'])
+            ->with(['client.user', 'provider.user', 'schedule'])
             ->orderBy('created_at', 'desc')
             ->get();
     }
@@ -30,7 +34,7 @@ class PaymentService
     public function findById(int $id): ?Payment
     {
         return Payment::query()
-            ->with(['client.user', 'provider.user'])
+            ->with(['client.user', 'provider.user', 'schedule'])
             ->find($id);
     }
 
@@ -65,11 +69,15 @@ class PaymentService
 
     //
 
+    public function platformFees(): array
+    {
+        return $this->pagarmePaymentService->platformFeeRates();
+    }
+
+    //
+
     public function payAcceptedSchedule(
-        Schedule $schedule,
-        string $paymentMethod,
-        ?int $clientPaymentMethodId = null,
-        array $options = []
+        Schedule $schedule, string $paymentMethod, ?int $clientPaymentMethodId = null, array $options = []
     ): Payment {
         $schedule->loadMissing(['client', 'provider', 'customSchedule.serviceType']);
 
@@ -133,8 +141,7 @@ class PaymentService
         }
 
         $clientPaymentMethod = null;
-
-        $cardId = null;
+        $cardId              = null;
 
         if ($paymentMethod === 'credit_card') {
             if (! $clientPaymentMethodId && empty($options['card_id'])) {
@@ -162,14 +169,11 @@ class PaymentService
 
         $serviceAmount = (float) $schedule->total_amount;
 
-        $platformFee = round($serviceAmount * 0.11, 2);
-        $grossAmount = round($serviceAmount + $platformFee, 2);
-
-        $platformRecipientId = config('services.pagarme.platform_recipient_id');
-
-        if ($platformFee > 0 && empty($platformRecipientId)) {
-            throw new \InvalidArgumentException('PAGARME_PLATFORM_RECIPIENT_ID precisa estar configurado para receber a taxa da plataforma no split.');
-        }
+        $amounts = $this->pagarmePaymentService->calculatePaymentAmounts(
+            serviceAmount: $serviceAmount,
+            paymentMethod: $paymentMethod,
+            schedule:      $schedule,
+        );
 
         $payment = Payment::create([
             'schedule_id'              => $schedule->id,
@@ -180,17 +184,17 @@ class PaymentService
             'gateway_code'             => 'payment-'.(string) Str::uuid(),
             'payment_method'           => $paymentMethod,
             'status'                   => PaymentStatusEnum::PENDING,
-            'gross_amount'             => $grossAmount,
+            'gross_amount'             => $amounts['gross_amount'],
             'gateway_fee_amount'       => 0,
-            'platform_fee_amount'      => $platformFee,
-            'net_amount'               => $grossAmount,
+            'platform_fee_amount'      => $amounts['platform_fee_amount'],
+            'net_amount'               => $amounts['gross_amount'],
             'currency'                 => 'BRL',
             'installments'             => 1,
             'expires_at'               => $paymentMethod === 'pix' ? Carbon::now()->addMinutes(30) : null,
 
             'metadata' => [
-                'service_amount' => number_format($serviceAmount, 2, '.', ''),
-                'platform_fee'   => number_format($platformFee, 2, '.', ''),
+                'service_amount' => number_format($amounts['service_amount'], 2, '.', ''),
+                'platform_fee'   => number_format($amounts['platform_fee_amount'], 2, '.', ''),
             ],
         ]);
 
@@ -214,11 +218,11 @@ class PaymentService
 
         try {
             $orderResponse = $this->pagarmePaymentService->processPayment(
-                payment: $payment,
-                schedule: $schedule,
+                payment:       $payment,
+                schedule:      $schedule,
                 paymentMethod: $paymentMethod,
-                cardId: $cardId,
-                options: $options,
+                cardId:        $cardId,
+                options:       $options,
             );
         } catch (\Throwable $e) {
             $payment->forceFill([
@@ -241,6 +245,40 @@ class PaymentService
         return $payment;
     }
 
+    /*
+    public function payCart(array $data, int $userId): SupportCollection
+    {
+        $cart = Cart::query()
+            ->with(['items.schedule.client', 'items.schedule.provider'])
+            ->findOrFail($data['cart_id']);
+
+        $schedules = $this->cartSchedules($cart);
+
+        if ($schedules->contains(fn (Schedule $schedule) => $schedule->client?->user_id !== $userId)) {
+            throw new AuthorizationException;
+        }
+
+        $this->validateCartSchedules($schedules, $data['payment_method']);
+
+        $payments = $schedules->map(fn (Schedule $schedule) => $this->payAcceptedSchedule(
+            schedule:              $schedule,
+            paymentMethod:         $data['payment_method'],
+            clientPaymentMethodId: $data['client_payment_method_id'] ?? null,
+
+            options: [
+                'phone'   => $data['phone']   ?? null,
+                'card_id' => $data['card_id'] ?? null,
+            ],
+        ));
+
+        $this->syncCartStatusAfterPayments($cart);
+
+        return $payments;
+    }
+    */
+
+    //
+
     public function getOrCreatePixPayment(Schedule $schedule): Payment
     {
         $existingPayment = Payment::query()
@@ -319,10 +357,120 @@ class PaymentService
 
     public function syncScheduleStatusAfterPayment(Schedule $schedule, Payment $payment): void
     {
-        if ($payment->status !== PaymentStatusEnum::PAID || $schedule->status === 'paid') {
+        if ($payment->status !== PaymentStatusEnum::PAID) {
+            return;
+        }
+
+        if ($schedule->status !== 'paid') {
+            $schedule->update(['status' => 'paid']);
+        }
+
+        $this->syncCartsForSchedule($schedule);
+    }
+
+    private function validateCartSchedules(SupportCollection $schedules, string $paymentMethod): void
+    {
+        if ($schedules->isEmpty()) {
+            throw new \InvalidArgumentException('Carrinho precisa ter ao menos um agendamento.');
+        }
+
+        if (! in_array($paymentMethod, ['credit_card', 'pix'], true)) {
+            throw new \InvalidArgumentException('Forma de pagamento invalida.');
+        }
+
+        $clientIds = $schedules->pluck('client_id')->unique()->values();
+
+        if ($clientIds->count() !== 1) {
+            throw new \InvalidArgumentException('Todos os agendamentos do carrinho precisam ser do mesmo cliente.');
+        }
+
+        $schedules->each(function (Schedule $schedule): void {
+            $schedule->loadMissing(['client', 'provider', 'customSchedule.serviceType']);
+
+            if ($schedule->status !== 'accepted') {
+                throw new \InvalidArgumentException("Agendamento {$schedule->id} precisa estar aceito para ser pago.");
+            }
+
+            if (! $schedule->provider_id || ! $schedule->provider) {
+                throw new \InvalidArgumentException("Agendamento {$schedule->id} precisa ter prestador confirmado para gerar pagamento.");
+            }
+
+            if ((float) $schedule->total_amount <= 0) {
+                throw new \InvalidArgumentException("Agendamento {$schedule->id} precisa ter valor maior que zero para gerar pagamento.");
+            }
+
+            if (empty($schedule->provider->recipient_id)) {
+                throw new \InvalidArgumentException("Prestador do agendamento {$schedule->id} precisa ter recipient_id do Pagar.me para receber split.");
+            }
+
+            $existingPayment = Payment::query()
+                ->where('schedule_id', $schedule->id)
+                ->whereIn('status', [
+                    PaymentStatusEnum::PENDING->value,
+                    PaymentStatusEnum::PROCESSING->value,
+                    PaymentStatusEnum::AUTHORIZED->value,
+                    PaymentStatusEnum::PAID->value,
+                ])
+                ->latest('id')
+                ->first();
+
+            if ($existingPayment) {
+                throw new \InvalidArgumentException("Ja existe um pagamento em andamento para o agendamento {$schedule->id}.");
+            }
+        });
+    }
+
+    //
+
+    private function cartSchedules(Cart $cart): SupportCollection
+    {
+        $schedules = $cart->items
+            ->map(fn ($item) => $item->schedule)
+            ->filter()
+            ->values();
+
+        if ($schedules->isEmpty() || $schedules->count() !== $cart->items->count()) {
+            throw new \InvalidArgumentException('Um ou mais agendamentos nao foram encontrados.');
+        }
+
+        return $schedules;
+    }
+
+    private function syncCartsForSchedule(Schedule $schedule): void
+    {
+        Cart::query()
+            ->whereHas('items', fn ($query) => $query->where('schedule_id', $schedule->id))
+            ->with('items')
+            ->get()
+            ->each(fn (Cart $cart) => $this->syncCartStatusAfterPayments($cart));
+    }
+
+    private function syncCartStatusAfterPayments(Cart $cart): void
+    {
+        $cart->loadMissing('items');
+
+        $scheduleIds = $cart->items
+            ->pluck('schedule_id')
+            ->filter()
+            ->unique()
+            ->values();
+
+        if ($scheduleIds->isEmpty()) {
             return;
         }
 
-        $schedule->update(['status' => 'paid']);
+        $paidSchedulesCount = Payment::query()
+            ->whereIn('schedule_id', $scheduleIds)
+            ->where('status', PaymentStatusEnum::PAID->value)
+            ->distinct('schedule_id')
+            ->count('schedule_id');
+
+        if ($paidSchedulesCount !== $scheduleIds->count()) {
+            return;
+        }
+
+        if ($cart->status !== CartStatusEnum::PAID) {
+            $cart->update(['status' => CartStatusEnum::PAID->value]);
+        }
     }
 }

+ 39 - 23
app/Services/ProviderWithdrawalService.php

@@ -19,6 +19,13 @@ class ProviderWithdrawalService
         private readonly PagarmeTransferService $pagarmeTransfer,
     ) {}
 
+    public function getWithdrawalFees(): array
+    {
+        return [
+            'payment_transfer_fee_amount' => $this->paymentTransferFeeAmount(),
+        ];
+    }
+
     public function getAvailableBalance(Provider $provider): float
     {
         $earnings = (float) PaymentSplit::query()
@@ -87,23 +94,35 @@ class ProviderWithdrawalService
                 throw new \RuntimeException('Saldo disponivel insuficiente para saque.');
             }
 
-            $amountInCents = (int) round($available * 100);
+            $paymentTransferFeeAmount = $this->paymentTransferFeeAmount();
+
+            $netAmount = max(0, round($available - $paymentTransferFeeAmount, 2));
+
+            if ($netAmount <= 0) {
+                throw new \RuntimeException('Saldo disponivel insuficiente para cobrir a taxa de transferencia.');
+            }
+
+            $amountInCents = (int) round($netAmount * 100);
 
             $idempotencyKey = sprintf('withdrawal-%d-%s', $provider->id, now()->timestamp);
 
             $bankAccount = $provider->recipient_default_bank_account;
 
             $withdrawal = ProviderWithdrawal::create([
-                'provider_id'     => $provider->id,
-                'recipient_id'    => $provider->recipient_id,
-                'idempotency_key' => $idempotencyKey,
-                'gross_amount'    => $available,
-                'net_amount'      => $available,
-                'status'          => ProviderWithdrawalStatusEnum::PENDING_TRANSFER,
-                'bank_account'    => $bankAccount,
-                'metadata'        => [
-                    'provider_id' => $provider->id,
-                    'amount'      => $available,
+                'provider_id'        => $provider->id,
+                'recipient_id'       => $provider->recipient_id,
+                'idempotency_key'    => $idempotencyKey,
+                'gross_amount'       => $available,
+                'gateway_fee_amount' => $paymentTransferFeeAmount,
+                'net_amount'         => $netAmount,
+                'status'             => ProviderWithdrawalStatusEnum::PENDING_TRANSFER,
+                'bank_account'       => $bankAccount,
+
+                'metadata' => [
+                    'provider_id'                  => $provider->id,
+                    'gross_amount'                 => $available,
+                    'payment_transfer_fee_amount'  => $paymentTransferFeeAmount,
+                    'net_amount'                   => $netAmount,
                 ],
             ]);
 
@@ -118,8 +137,6 @@ class ProviderWithdrawalService
                     'transfer_id'        => $transfer->id,
                     'status'             => $transfer->status ?? ProviderWithdrawalStatusEnum::PROCESSING->value,
                     'type'               => $transfer->type,
-                    'gateway_fee_amount' => $transfer->fee ?? 0,
-                    'net_amount'         => max(0, $available - ($transfer->fee ?? 0) / 100),
                     'gateway_payload'    => $transfer->toArray(),
                 ])->save();
 
@@ -217,7 +234,7 @@ class ProviderWithdrawalService
             ->when(
                 ! app()->environment('local', 'development'),
                 fn ($query) => $query->whereRaw(
-                    $this->scheduleEndedAtExpression().' <= ?',
+                    '(date + end_time) <= ?',
                     [$this->withdrawalReleaseCutoff()]
                 )
             );
@@ -230,23 +247,22 @@ class ProviderWithdrawalService
             ->where(function ($q) {
                 $q->where('code_verified', false)
                     ->orWhereRaw(
-                        $this->scheduleEndedAtExpression().' > ?',
+                        '(date + end_time) > ?',
                         [$this->withdrawalReleaseCutoff()]
                     );
             });
     }
 
-    private function scheduleEndedAtExpression(): string
+    private function withdrawalReleaseCutoff(): string
     {
-        return match (DB::connection()->getDriverName()) {
-            'pgsql'  => '(date + end_time)',
-            'sqlite' => "datetime(date || ' ' || end_time)",
-            default  => 'TIMESTAMP(date, end_time)',
-        };
+        $releaseDays = max(0, (int) config('services.pagarme.withdrawal_release_days'));
+
+        return Carbon::now()->subDays($releaseDays)->format('Y-m-d H:i:s');
     }
 
-    private function withdrawalReleaseCutoff(): string
+    private function paymentTransferFeeAmount(): float
     {
-        return Carbon::now()->subDays(5)->format('Y-m-d H:i:s');
+        return round((float) config('services.pagarme.transfer_fee_amount'), 2);
     }
+
 }

+ 217 - 243
app/Services/ScheduleService.php

@@ -25,15 +25,6 @@ class ScheduleService
             ->get();
     }
 
-    public function getFinished()
-    {
-        return Schedule::with(['client.user', 'provider.user'])
-            ->where('status', 'finished')
-            ->orderBy('date', 'desc')
-            ->orderBy('start_time', 'desc')
-            ->get();
-    }
-
     public function getById($id)
     {
         return Schedule::with(['client.user', 'provider.user', 'address'])->findOrFail($id);
@@ -42,13 +33,11 @@ class ScheduleService
     public function createSingleOrMultiple(array $baseData, array $schedules)
     {
         try {
-
             DB::beginTransaction();
 
             $createdSchedules = [];
 
             foreach ($schedules as $schedule) {
-
                 $datasMerged = array_merge($baseData, $schedule);
 
                 $this->validateProviderAvailability($datasMerged, null);
@@ -59,23 +48,18 @@ class ScheduleService
 
                 $newSchedule = Schedule::create($scheduleData);
 
-                /* NOTIFICAÇÃO PRESTADOR */
-                if ($newSchedule->provider_id) {
+                // NOTIFICAÇÃO PRESTADOR
 
+                if ($newSchedule->provider_id) {
                     $notificationService = app(NotificationService::class);
 
                     $notificationService->create([
-                        'title' => 'Nova solicitação de diária!',
-
+                        'title'       => 'Nova solicitação de diária!',
                         'description' => 'Você recebeu uma nova solicitação de diária.',
-
-                        'origin' => 'schedule',
-
-                        'origin_id' => $newSchedule->id,
-
-                        'type' => NotificationTypeEnum::SCHEDULE_PROVIDER_CLIENT_NEW_SOLICITATION->value,
-
-                        'user_id' => $newSchedule->provider->user_id,
+                        'origin'      => 'schedule',
+                        'origin_id'   => $newSchedule->id,
+                        'type'        => NotificationTypeEnum::SCHEDULE_PROVIDER_CLIENT_NEW_SOLICITATION->value,
+                        'user_id'     => $newSchedule->provider->user_id,
                     ]);
                 }
 
@@ -84,7 +68,6 @@ class ScheduleService
 
             DB::commit();
         } catch (\Exception $e) {
-
             DB::rollBack();
 
             throw new \Exception(__($e->getMessage()));
@@ -100,7 +83,9 @@ class ScheduleService
         if (isset($data['provider_id']) || isset($data['period_type'])) {
             $providerId = $data['provider_id'] ?? $schedule->provider_id;
             $periodType = $data['period_type'] ?? $schedule->period_type;
+
             $provider = Provider::findOrFail($providerId);
+
             $data['total_amount'] = $this->calculateAmount($provider, $periodType);
         }
 
@@ -124,84 +109,55 @@ class ScheduleService
         return $schedule;
     }
 
-    private function calculateAmount(Provider $provider, string $periodType): float
-    {
-        $hourlyRates = [
-            '2' => $provider->daily_price_2h ?? 0,
-            '4' => $provider->daily_price_4h ?? 0,
-            '6' => $provider->daily_price_6h ?? 0,
-            '8' => $provider->daily_price_8h ?? 0,
-        ];
-
-        return $hourlyRates[$periodType] ?? 0;
-    }
+    //
 
-    private function validateProviderAvailability(array $data, $excludeScheduleId = null)
+    public function getClientProviderBlocks(int $clientId, int $providerId): array
     {
-        $provider_id = $data['provider_id'];
-        $client_id = $data['client_id'];
-
-        $date = Carbon::parse($data['date']);
-        $dayOfWeek = $date->dayOfWeek;
-        $startTime = $data['start_time'];
-        $endTime = $data['end_time'];
-        $date_ymd = $date->format('Y-m-d');
-        $period = $startTime < '13:00:00' ? 'morning' : 'afternoon';
-
-        // bloqueio 2 schedules por semana para o mesmo client e provider
-        ScheduleBusinessRules::validateWeeklyScheduleLimit(
-            $client_id,
-            $provider_id,
-            $data['date'],
-            $excludeScheduleId
-        );
-
-        // bloqueio provider trabalha no dia/periodo
-        ScheduleBusinessRules::validateWorkingDay(
-            $provider_id,
-            $dayOfWeek,
-            $period
-        );
-
-        // bloqueio provider tem blockedday para dia/hora
-        ScheduleBusinessRules::validateBlockedDay(
-            $provider_id,
-            $date->format('Y-m-d'),
-            $startTime,
-            $endTime
-        );
+        $today = Carbon::today()->format('Y-m-d');
 
-        // bloqueio provider tem outro agendamento para dia/hora
-        ScheduleBusinessRules::validateConflictingSchedule(
-            $provider_id,
-            $date->format('Y-m-d'),
-            $startTime,
-            $endTime,
-            $excludeScheduleId
-        );
+        $schedules = Schedule::where('client_id', $clientId)
+            ->where('provider_id', $providerId)
+            ->whereNotIn('status', self::EXCLUDED_STATUSES)
+            ->whereDate('date', '>=', $today)
+            ->orderBy('date')
+            ->orderBy('start_time')
+            ->get(['id', 'date', 'start_time', 'end_time', 'status']);
 
-        // bloqueio provider tem outra proposta na mesma data
-        ScheduleBusinessRules::validateConflictingProposalSameDate(
-            $provider_id,
-            $date_ymd,
-            $startTime,
-            $endTime,
-            null
-        );
+        $existingSchedules = $schedules->map(function ($schedule) {
+            return [
+                'id'         => $schedule->id,
+                'date'       => Carbon::parse($schedule->date)->format('Y-m-d'),
+                'start_time' => $schedule->start_time,
+                'end_time'   => $schedule->end_time,
+                'status'     => $schedule->status,
+            ];
+        })->values();
 
-        // bloqueio caso o client tenha bloqueado o provider
-        ScheduleBusinessRules::validateClientNotBlockedByProvider(
-            $client_id,
-            $provider_id
-        );
+        $fullyBlockedWeeks = $schedules
+            ->groupBy(function ($schedule) {
+                return Carbon::parse($schedule->date)
+                    ->startOfWeek(Carbon::SUNDAY)
+                    ->format('Y-m-d');
+            })
+            ->filter(function ($weekSchedules) {
+                return $weekSchedules->count() >= 2;
+            })
+            ->keys()
+            ->values();
 
-        // bloqueio caso o provider tenha bloqueado o client
-        ScheduleBusinessRules::validateProviderNotBlockedByClient(
-            $client_id,
-            $provider_id
-        );
+        return [
+            'existing_schedules'  => $existingSchedules,
+            'fully_blocked_weeks' => $fullyBlockedWeeks,
+        ];
+    }
 
-        return true;
+    public function getFinished()
+    {
+        return Schedule::with(['client.user', 'provider.user'])
+            ->where('status', 'finished')
+            ->orderBy('date', 'desc')
+            ->orderBy('start_time', 'desc')
+            ->get();
     }
 
     public function getSchedulesDefaultGroupedByClient()
@@ -220,7 +176,8 @@ class ScheduleService
             return [
                 'client_id'   => $firstSchedule->client_id,
                 'client_name' => $firstSchedule->client->user->name ?? 'N/A',
-                'schedules'   => $clientSchedules->map(function ($schedule) {
+
+                'schedules' => $clientSchedules->map(function ($schedule) {
                     return [
                         'id'            => $schedule->id,
                         'date'          => $schedule->date ? Carbon::parse($schedule->date)->format('d/m/Y') : null,
@@ -234,16 +191,19 @@ class ScheduleService
                         'client_id'     => $schedule->client_id,
                         'provider_id'   => $schedule->provider_id,
                         'provider_name' => $schedule->provider->user->name ?? 'N/A',
-                        'address'       => $schedule->address ? [
+
+                        'address' => $schedule->address ? [
                             'id'         => $schedule->address->id,
                             'address'    => $schedule->address->address,
                             'complement' => $schedule->address->complement,
                             'zip_code'   => $schedule->address->zip_code,
-                            'city'       => $schedule->address->city->name ?? '',
+                            'city'       => $schedule->address->city->name        ?? '',
                             'state'      => $schedule->address->city->state->name ?? '',
                         ] : null,
+
                         'client_name' => $schedule->client->user->name ?? 'N/A',
-                        'reviews'     => $schedule->reviews->map(function ($review) {
+
+                        'reviews' => $schedule->reviews->map(function ($review) {
                             return [
                                 'id'           => $review->id,
                                 'stars'        => $review->stars,
@@ -252,6 +212,7 @@ class ScheduleService
                                 'origin_id'    => $review->origin_id,
                                 'created_at'   => Carbon::parse($review->created_at)->format('Y-m-d H:i'),
                                 'updated_at'   => Carbon::parse($review->updated_at)->format('Y-m-d H:i'),
+
                                 'improvements' => $review->reviewsImprovements->map(function ($ri) {
                                     return [
                                         'id'                    => $ri->id,
@@ -269,10 +230,46 @@ class ScheduleService
         return $grouped;
     }
 
+    //
+
+    public function cancelWithReason(int $id, string $cancelText)
+    {
+        try {
+            DB::beginTransaction();
+
+            $schedule = Schedule::findOrFail($id);
+
+            $allowedStatuses = ['accepted', 'paid', 'pending'];
+
+            if (! in_array($schedule->status, $allowedStatuses)) {
+                throw new \Exception("Cancelamento não permitido para o status atual: {$schedule->status}");
+            }
+
+            $cancelled_by = Auth::user()->type;
+
+            $schedule->update([
+                'status'       => 'cancelled',
+                'cancel_text'  => $cancelText,
+                'cancelled_by' => $cancelled_by,
+            ]);
+
+            DB::commit();
+
+            return $schedule->fresh(['client.user', 'provider.user', 'address']);
+        } catch (\Exception $e) {
+            DB::rollBack();
+
+            Log::error('Erro ao cancelar agendamento: '.$e->getMessage());
+
+            throw new \Exception('Não foi possível cancelar o agendamento.');
+        }
+    }
+
     public function updateStatus($id, string $status)
     {
         try {
             DB::beginTransaction();
+
             $schedule = Schedule::findOrFail($id);
 
             $allowedTransitions = [
@@ -298,73 +295,54 @@ class ScheduleService
             $schedule->update(['status' => $status]);
 
             switch ($status) {
-
                 case 'pending':
-
                     break;
 
                 case 'accepted':
-
                     $notificationService = app(NotificationService::class);
 
                     // CLIENTE
-                    $notificationService->create([
-                        'title' => 'Agendamento aceito!',
-
-                        'description' => $schedule->provider->user->name.
-                            ' aceitou sua solicitação de diária.',
 
-                        'origin' => 'schedule',
-
-                        'origin_id' => $schedule->id,
-
-                        'type' => NotificationTypeEnum::SCHEDULE_CLIENT_PROVIDER_ACCEPTED->value,
-
-                        'user_id' => $schedule->client->user_id,
+                    $notificationService->create([
+                        'title'       => 'Agendamento aceito!',
+                        'description' => $schedule->provider->user->name. ' aceitou sua solicitação de diária.',
+                        'origin'      => 'schedule',
+                        'origin_id'   => $schedule->id,
+                        'type'        => NotificationTypeEnum::SCHEDULE_CLIENT_PROVIDER_ACCEPTED->value,
+                        'user_id'     => $schedule->client->user_id,
                     ]);
 
                     break;
 
                 case 'rejected':
-
                     $notificationService = app(NotificationService::class);
 
                     // CLIENTE
-                    $notificationService->create([
-                        'title' => 'Agendamento recusado!',
 
+                    $notificationService->create([
+                        'title'       => 'Agendamento recusado!',
                         'description' => 'O diarista não poderá atender. Veja outros profissionais disponíveis.',
-
-                        'origin' => 'schedule',
-
-                        'origin_id' => $schedule->id,
-
-                        'type' => NotificationTypeEnum::SCHEDULE_CLIENT_PROVIDER_REFUSED->value,
-
-                        'user_id' => $schedule->client->user_id,
+                        'origin'      => 'schedule',
+                        'origin_id'   => $schedule->id,
+                        'type'        => NotificationTypeEnum::SCHEDULE_CLIENT_PROVIDER_REFUSED->value,
+                        'user_id'     => $schedule->client->user_id,
                     ]);
 
                     break;
 
                 case 'paid':
-
                     $notificationService = app(NotificationService::class);
 
                     // PRESTADOR
-                    if ($schedule->provider_id) {
 
+                    if ($schedule->provider_id) {
                         $notificationService->create([
-                            'title' => 'Pagamento confirmado!',
-
+                            'title'       => 'Pagamento confirmado!',
                             'description' => 'O cliente confirmou o pagamento da diária.',
-
-                            'origin' => 'schedule',
-
-                            'origin_id' => $schedule->id,
-
-                            'type' => NotificationTypeEnum::SCHEDULE_PROVIDER_START->value,
-
-                            'user_id' => $schedule->provider->user_id,
+                            'origin'      => 'schedule',
+                            'origin_id'   => $schedule->id,
+                            'type'        => NotificationTypeEnum::SCHEDULE_PROVIDER_START->value,
+                            'user_id'     => $schedule->provider->user_id,
                         ]);
                     }
 
@@ -381,47 +359,36 @@ class ScheduleService
                     break;
 
                 case 'cancelled':
-
                     $notificationService = app(NotificationService::class);
 
                     switch (Auth::user()->type) {
                         case 'client':
-
                             // PRESTADOR
-                            if ($schedule->provider_id) {
 
-                                $notificationService->create(['title' => 'Agendamento cancelado!',
+                            if ($schedule->provider_id) {
 
+                                $notificationService->create([
+                                    'title'       => 'Agendamento cancelado!',
                                     'description' => 'O cliente cancelou a diária.',
-
-                                    'origin' => 'schedule',
-
-                                    'origin_id' => $schedule->id,
-
-                                    'type' => NotificationTypeEnum::SCHEDULE_PROVIDER_CLIENT_CANCELLED->value,
-
-                                    'user_id' => $schedule->provider->user_id,
+                                    'origin'      => 'schedule',
+                                    'origin_id'   => $schedule->id,
+                                    'type'        => NotificationTypeEnum::SCHEDULE_PROVIDER_CLIENT_CANCELLED->value,
+                                    'user_id'     => $schedule->provider->user_id,
                                 ]);
                             }
 
                             break;
 
                         case 'provider':
-
                             // CLIENTE
-                            $notificationService->create([
-                                'title' => 'Agendamento cancelado!',
-
-                                'description' => $schedule->provider->user->name.
-                                    ' cancelou a diária.',
-
-                                'origin' => 'schedule',
-
-                                'origin_id' => $schedule->id,
-
-                                'type' => NotificationTypeEnum::SCHEDULE_CLIENT_PROVIDER_CANCELLED->value,
 
-                                'user_id' => $schedule->client->user_id,
+                            $notificationService->create([
+                                'title'       => 'Agendamento cancelado!',
+                                'description' => $schedule->provider->user->name. ' cancelou a diária.',
+                                'origin'      => 'schedule',
+                                'origin_id'   => $schedule->id,
+                                'type'        => NotificationTypeEnum::SCHEDULE_CLIENT_PROVIDER_CANCELLED->value,
+                                'user_id'     => $schedule->client->user_id,
                             ]);
 
                             break;
@@ -433,45 +400,33 @@ class ScheduleService
                     break;
 
                 case 'started':
-
                     $notificationService = app(NotificationService::class);
 
                     // CLIENTE
-                    $notificationService->create([
-                        'title' => 'Diarista a caminho!',
-
-                        'description' => 'Informe o código '.
-                            $schedule->code.
-                            ' para confirmar a chegada da diarista e liberar o início do serviço.',
-
-                        'origin' => 'schedule',
-
-                        'origin_id' => $schedule->id,
 
-                        'type' => NotificationTypeEnum::SCHEDULE_CLIENT_PROVIDER_COMING->value,
-
-                        'user_id' => $schedule->client->user_id,
+                    $notificationService->create([
+                        'title'       => 'Diarista a caminho!',
+                        'description' => 'Informe o código '. $schedule->code. ' para confirmar a chegada da diarista e liberar o início do serviço.',
+                        'origin'      => 'schedule',
+                        'origin_id'   => $schedule->id,
+                        'type'        => NotificationTypeEnum::SCHEDULE_CLIENT_PROVIDER_COMING->value,
+                        'user_id'     => $schedule->client->user_id,
                     ]);
 
                     break;
 
                 case 'finished':
-
                     $notificationService = app(NotificationService::class);
 
                     // CLIENTE
-                    $notificationService->create([
-                        'title' => 'Diária finalizada!',
 
+                    $notificationService->create([
+                        'title'       => 'Diária finalizada!',
                         'description' => 'Avalie o serviço feito pelo diarista e conte-nos como foi sua experiência.',
-
-                        'origin' => 'schedule',
-
-                        'origin_id' => $schedule->id,
-
-                        'type' => NotificationTypeEnum::SCHEDULE_CLIENT_PROVIDER_FINISHED->value,
-
-                        'user_id' => $schedule->client->user_id,
+                        'origin'      => 'schedule',
+                        'origin_id'   => $schedule->id,
+                        'type'        => NotificationTypeEnum::SCHEDULE_CLIENT_PROVIDER_FINISHED->value,
+                        'user_id'     => $schedule->client->user_id,
                     ]);
 
                     break;
@@ -482,81 +437,100 @@ class ScheduleService
             return $schedule->fresh(['client.user', 'provider.user', 'address']);
         } catch (\Exception $e) {
             DB::rollBack();
+
             Log::error('Erro ao atualizar status do agendamento: '.$e->getMessage());
+
             throw new \Exception('Não foi possível atualizar o status do agendamento.');
         }
     }
 
-    public function cancelWithReason(int $id, string $cancelText)
+    //
+
+    private function calculateAmount(Provider $provider, string $periodType): float
     {
-        try {
-            DB::beginTransaction();
+        $hourlyRates = [
+            '2' => $provider->daily_price_2h ?? 0,
+            '4' => $provider->daily_price_4h ?? 0,
+            '6' => $provider->daily_price_6h ?? 0,
+            '8' => $provider->daily_price_8h ?? 0,
+        ];
 
-            $schedule = Schedule::findOrFail($id);
+        return $hourlyRates[$periodType] ?? 0;
+    }
 
-            $allowedStatuses = ['accepted', 'paid', 'pending'];
+    private function validateProviderAvailability(array $data, $excludeScheduleId = null)
+    {
+        $provider_id = $data['provider_id'];
+        $client_id   = $data['client_id'];
 
-            if (! in_array($schedule->status, $allowedStatuses)) {
-                throw new \Exception("Cancelamento não permitido para o status atual: {$schedule->status}");
-            }
+        $date = Carbon::parse($data['date']);
 
-            $cancelled_by = Auth::user()->type;
+        $dayOfWeek = $date->dayOfWeek;
+        $startTime = $data['start_time'];
+        $endTime   = $data['end_time'];
+        $date_ymd  = $date->format('Y-m-d');
+        $period    = $startTime < '13:00:00' ? 'morning' : 'afternoon';
 
-            $schedule->update([
-                'status'       => 'cancelled',
-                'cancel_text'  => $cancelText,
-                'cancelled_by' => $cancelled_by,
-            ]);
+        // bloqueio 2 schedules por semana para o mesmo client e provider
 
-            DB::commit();
+        ScheduleBusinessRules::validateWeeklyScheduleLimit(
+            $client_id,
+            $provider_id,
+            $data['date'],
+            $excludeScheduleId
+        );
 
-            return $schedule->fresh(['client.user', 'provider.user', 'address']);
-        } catch (\Exception $e) {
-            DB::rollBack();
+        // bloqueio provider trabalha no dia/periodo
 
-            Log::error('Erro ao cancelar agendamento: '.$e->getMessage());
+        ScheduleBusinessRules::validateWorkingDay(
+            $provider_id,
+            $dayOfWeek,
+            $period
+        );
 
-            throw new \Exception('Não foi possível cancelar o agendamento.');
-        }
-    }
+        // bloqueio provider tem blockedday para dia/hora
 
-    public function getClientProviderBlocks(int $clientId, int $providerId): array
-    {
-        $today = Carbon::today()->format('Y-m-d');
+        ScheduleBusinessRules::validateBlockedDay(
+            $provider_id,
+            $date->format('Y-m-d'),
+            $startTime,
+            $endTime
+        );
 
-        $schedules = Schedule::where('client_id', $clientId)
-            ->where('provider_id', $providerId)
-            ->whereNotIn('status', self::EXCLUDED_STATUSES)
-            ->whereDate('date', '>=', $today)
-            ->orderBy('date')
-            ->orderBy('start_time')
-            ->get(['id', 'date', 'start_time', 'end_time', 'status']);
+        // bloqueio provider tem outro agendamento para dia/hora
 
-        $existingSchedules = $schedules->map(function ($schedule) {
-            return [
-                'id'         => $schedule->id,
-                'date'       => Carbon::parse($schedule->date)->format('Y-m-d'),
-                'start_time' => $schedule->start_time,
-                'end_time'   => $schedule->end_time,
-                'status'     => $schedule->status,
-            ];
-        })->values();
+        ScheduleBusinessRules::validateConflictingSchedule(
+            $provider_id,
+            $date->format('Y-m-d'),
+            $startTime,
+            $endTime,
+            $excludeScheduleId
+        );
 
-        $fullyBlockedWeeks = $schedules
-            ->groupBy(function ($schedule) {
-                return Carbon::parse($schedule->date)
-                    ->startOfWeek(Carbon::SUNDAY)
-                    ->format('Y-m-d');
-            })
-            ->filter(function ($weekSchedules) {
-                return $weekSchedules->count() >= 2;
-            })
-            ->keys()
-            ->values();
+        // bloqueio provider tem outra proposta na mesma data
 
-        return [
-            'existing_schedules'  => $existingSchedules,
-            'fully_blocked_weeks' => $fullyBlockedWeeks,
-        ];
+        ScheduleBusinessRules::validateConflictingProposalSameDate(
+            $provider_id,
+            $date_ymd,
+            $startTime,
+            $endTime,
+            null
+        );
+
+        // bloqueio caso o client tenha bloqueado o provider
+
+        ScheduleBusinessRules::validateClientNotBlockedByProvider(
+            $client_id,
+            $provider_id
+        );
+
+        // bloqueio caso o provider tenha bloqueado o client
+
+        ScheduleBusinessRules::validateProviderNotBlockedByClient(
+            $client_id,
+            $provider_id
+        );
+
+        return true;
     }
 }

+ 23 - 13
app/Services/WebhookService.php

@@ -30,16 +30,18 @@ class WebhookService
     ];
 
     public function __construct(
-        private readonly PagarmePaymentService $pagarmePaymentService,
-        private readonly PaymentService $paymentService,
+        private readonly PagarmePaymentService     $pagarmePaymentService,
+        private readonly PaymentService            $paymentService,
         private readonly ProviderWithdrawalService $providerWithdrawalService,
-    ) {}
+    ) {
+    }
 
     public function handlePagarme(array $payload): ?Payment
     {
         $event = $payload['type'] ?? null;
-        $data = $payload['data'] ?? [];
-        $data = is_array($data) ? $data : [];
+        $data  = $payload['data'] ?? [];
+        $data  = is_array($data) ? $data : [];
+
         $webhook = $this->recordWebhook($payload, $event, $data);
 
         Log::channel('pagarme')->info('Pagar.me webhook received', [
@@ -68,6 +70,7 @@ class WebhookService
         }
 
         $orderResponse = $this->normalizeOrderResponse($event, $data);
+
         $payment = $this->findPayment($orderResponse);
 
         if (! $payment) {
@@ -91,7 +94,11 @@ class WebhookService
         try {
             $payment = $this->pagarmePaymentService->applyGatewayResponseToPayment($payment, $orderResponse);
 
-            $this->paymentService->syncScheduleStatusAfterPayment($payment->schedule, $payment);
+            $payment->loadMissing('schedule');
+
+            if ($payment->schedule) {
+                $this->paymentService->syncScheduleStatusAfterPayment($payment->schedule, $payment);
+            }
 
             $webhook->update([
                 'payment_id'   => $payment->id,
@@ -112,9 +119,12 @@ class WebhookService
         }
     }
 
+    //
+
     private function recordWebhook(array $payload, ?string $event, array $data): Webhook
     {
         $references = $this->extractReferences($event, $data);
+
         $hookId = $payload['id'] ?? null;
 
         $attributes = [
@@ -191,14 +201,14 @@ class WebhookService
 
     private function findPayment(array $orderResponse): ?Payment
     {
-        $charge = $orderResponse['charges'][0] ?? [];
-        $transaction = $charge['last_transaction'] ?? [];
-        $metadata = $orderResponse['metadata'] ?? $charge['metadata'] ?? [];
-        $metadataId = filter_var($metadata['payment_id'] ?? null, FILTER_VALIDATE_INT) ?: null;
-        $chargeId = $charge['id'] ?? null;
+        $charge        = $orderResponse['charges'][0] ?? [];
+        $transaction   = $charge['last_transaction'] ?? [];
+        $metadata      = $orderResponse['metadata'] ?? $charge['metadata'] ?? [];
+        $metadataId    = filter_var($metadata['payment_id'] ?? null, FILTER_VALIDATE_INT) ?: null;
+        $chargeId      = $charge['id'] ?? null;
         $transactionId = $transaction['id'] ?? null;
-        $orderId = $orderResponse['id'] ?? null;
-        $references = array_filter([$metadataId, $chargeId, $transactionId, $orderId]);
+        $orderId       = $orderResponse['id'] ?? null;
+        $references    = array_filter([$metadataId, $chargeId, $transactionId, $orderId]);
 
         if (empty($references)) {
             return null;

+ 0 - 1
composer.json

@@ -19,7 +19,6 @@
     "require-dev": {
         "barryvdh/laravel-ide-helper": "^3.6",
         "fakerphp/faker": "^1.23",
-        "laravel/pint": "^1.13",
         "laravel/sail": "^1.26",
         "mockery/mockery": "^1.6",
         "nunomaduro/collision": "^8.0",

+ 4 - 73
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "73e33a3fb94ef20d8acfd663e2eea0c5",
+    "content-hash": "2d12a35327a589e43b4d781782e651a2",
     "packages": [
         {
             "name": "aws/aws-crt-php",
@@ -8292,75 +8292,6 @@
             },
             "time": "2025-04-30T06:54:44+00:00"
         },
-        {
-            "name": "laravel/pint",
-            "version": "v1.24.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/laravel/pint.git",
-                "reference": "0345f3b05f136801af8c339f9d16ef29e6b4df8a"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/laravel/pint/zipball/0345f3b05f136801af8c339f9d16ef29e6b4df8a",
-                "reference": "0345f3b05f136801af8c339f9d16ef29e6b4df8a",
-                "shasum": ""
-            },
-            "require": {
-                "ext-json": "*",
-                "ext-mbstring": "*",
-                "ext-tokenizer": "*",
-                "ext-xml": "*",
-                "php": "^8.2.0"
-            },
-            "require-dev": {
-                "friendsofphp/php-cs-fixer": "^3.82.2",
-                "illuminate/view": "^11.45.1",
-                "larastan/larastan": "^3.5.0",
-                "laravel-zero/framework": "^11.45.0",
-                "mockery/mockery": "^1.6.12",
-                "nunomaduro/termwind": "^2.3.1",
-                "pestphp/pest": "^2.36.0"
-            },
-            "bin": [
-                "builds/pint"
-            ],
-            "type": "project",
-            "autoload": {
-                "files": [
-                    "overrides/Runner/Parallel/ProcessFactory.php"
-                ],
-                "psr-4": {
-                    "App\\": "app/",
-                    "Database\\Seeders\\": "database/seeders/",
-                    "Database\\Factories\\": "database/factories/"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Nuno Maduro",
-                    "email": "enunomaduro@gmail.com"
-                }
-            ],
-            "description": "An opinionated code formatter for PHP.",
-            "homepage": "https://laravel.com",
-            "keywords": [
-                "format",
-                "formatter",
-                "lint",
-                "linter",
-                "php"
-            ],
-            "support": {
-                "issues": "https://github.com/laravel/pint/issues",
-                "source": "https://github.com/laravel/pint"
-            },
-            "time": "2025-07-10T18:09:32+00:00"
-        },
         {
             "name": "laravel/sail",
             "version": "v1.43.1",
@@ -10343,12 +10274,12 @@
     ],
     "aliases": [],
     "minimum-stability": "stable",
-    "stability-flags": [],
+    "stability-flags": {},
     "prefer-stable": true,
     "prefer-lowest": false,
     "platform": {
         "php": "^8.3"
     },
-    "platform-dev": [],
-    "plugin-api-version": "2.3.0"
+    "platform-dev": {},
+    "plugin-api-version": "2.6.0"
 }

+ 7 - 2
config/services.php

@@ -46,7 +46,12 @@ return [
         'webhook_user'          => env('PAGARME_WEBHOOK_USER'),
         'webhook_password'      => env('PAGARME_WEBHOOK_PASSWORD'),
         'platform_recipient_id' => env('PAGARME_PLATFORM_RECIPIENT_ID'),
-        'pix_disable_split'     => env('PAGARME_PIX_DISABLE_SPLIT', false),
-    ],
 
+        'pix_disable_split'                      => env('PAGARME_PIX_DISABLE_SPLIT', false),
+        'platform_pix_fee_rate'                  => env('PAGARME_PLATFORM_PIX_FEE_RATE', 0.12),
+        'platform_credit_card_fee_rate'          => env('PAGARME_PLATFORM_CREDIT_CARD_FEE_RATE', 0.16),
+        'platform_cart_min_3_schedules_fee_rate' => env('PAGARME_PLATFORM_CART_MIN_3_SCHEDULES_FEE_RATE', 0.10),
+        'transfer_fee_amount'                    => env('PAGARME_TRANSFER_FEE_AMOUNT', 3.67),
+        'withdrawal_release_days'                => env('PAGARME_WITHDRAWAL_RELEASE_DAYS', 2),
+    ],
 ];

Vissa filer visades inte eftersom för många filer har ändrats