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

feat: ✨ feat (contas bancarias prestador) adicionada possibilidade de ter mais de uma conta bancaria

adicionada possibilidade de ter multiplas contas bancarias no prestador, com funcao de atribuir principal, e foi removido campos de pix

fase:dev | origin:escopo
Gustavo Zanatta 1 день назад
Родитель
Сommit
3ceed35388
34 измененных файлов с 1318 добавлено и 259 удалено
  1. 0 1
      .env.example
  2. 4 2
      app/Commands/RefreshPagarmeEntities.php
  3. 20 0
      app/Exceptions/BankAccountException.php
  4. 80 0
      app/Http/Controllers/ProviderBankAccountController.php
  5. 0 22
      app/Http/Controllers/ProviderController.php
  6. 0 7
      app/Http/Controllers/ProviderWithdrawalController.php
  7. 0 31
      app/Http/Requests/ProviderBankAccountRequest.php
  8. 27 0
      app/Http/Requests/ProviderBankAccountStoreRequest.php
  9. 28 0
      app/Http/Requests/ProviderBankAccountUpdateRequest.php
  10. 0 51
      app/Http/Requests/ProviderPaymentMethodRequest.php
  11. 4 4
      app/Http/Requests/ProviderRequest.php
  12. 4 5
      app/Http/Requests/RegisterProviderRequest.php
  13. 46 0
      app/Http/Resources/ProviderBankAccountResource.php
  14. 0 23
      app/Http/Resources/ProviderPaymentMethodResource.php
  15. 4 0
      app/Http/Resources/ProviderResource.php
  16. 33 4
      app/Models/Provider.php
  17. 151 0
      app/Models/ProviderBankAccount.php
  18. 1 1
      app/Services/AuthService.php
  19. 2 3
      app/Services/ClientFavoriteProviderService.php
  20. 2 6
      app/Services/DashboardService.php
  21. 51 2
      app/Services/Pagarme/PagarmeRecipientService.php
  22. 374 0
      app/Services/ProviderBankAccountService.php
  23. 7 81
      app/Services/ProviderService.php
  24. 6 2
      app/Services/ProviderWithdrawalService.php
  25. 1 1
      app/Services/UserService.php
  26. 0 5
      config/withdrawals.php
  27. 128 0
      database/migrations/2026_08_12_100000_create_provider_bank_accounts_table.php
  28. 11 0
      lang/en/messages.php
  29. 11 0
      lang/es/messages.php
  30. 11 0
      lang/pt/messages.php
  31. 4 2
      phpunit.xml
  32. 3 6
      routes/authRoutes/provider.php
  33. 14 0
      routes/authRoutes/provider_bank_account.php
  34. 291 0
      tests/Feature/ProviderBankAccountTest.php

+ 0 - 1
.env.example

@@ -41,7 +41,6 @@ FIREBASE_CREDENTIALS=/var/www/sfp_api_laravel_diarista/storage/diariaappsfp-fire
 FIREBASE_PROJECT=diariaappsfp
 FIREBASE_CACHE_STORE=redis
 
-PIX_WITHDRAWALS_ENABLED=false
 PROVIDER_DAILY_PRICE_MIN=18
 SMS_ENABLED=false
 

+ 4 - 2
app/Commands/RefreshPagarmeEntities.php

@@ -76,7 +76,7 @@ class RefreshPagarmeEntities extends Command
 
         $providers = Provider::query()
             ->whereNotNull('recipient_id')
-            ->with(['user', 'addresses.city.state', 'addresses.state'])
+            ->with(['user', 'addresses.city.state', 'addresses.state', 'primaryBankAccount'])
             ->get();
 
         $bar = $this->output->createProgressBar($providers->count());
@@ -94,7 +94,9 @@ class RefreshPagarmeEntities extends Command
                 'recipient_payment_mode'         => $provider->recipient_payment_mode,
                 'recipient_description'          => $provider->recipient_description,
                 'recipient_metadata'             => $provider->recipient_metadata ?? [],
-                'recipient_default_bank_account' => $provider->recipient_default_bank_account ?? [],
+                'recipient_default_bank_account' => $provider->primaryBankAccount?->toGatewayBankAccount()
+                    ?? $provider->recipient_default_bank_account
+                    ?? [],
                 'birth_date'                     => $provider->birth_date?->format('Y-m-d'),
                 'professional_occupation'        => 'autonomo',
                 'phone'                          => $provider->user?->phone,

+ 20 - 0
app/Exceptions/BankAccountException.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace App\Exceptions;
+
+use Illuminate\Http\JsonResponse;
+use Illuminate\Http\Request;
+use RuntimeException;
+
+class BankAccountException extends RuntimeException
+{
+    public function __construct(string $messageKey, private readonly int $status = 422)
+    {
+        parent::__construct(__("messages.{$messageKey}"));
+    }
+
+    public function render(Request $request): JsonResponse
+    {
+        return response()->json(['payload' => null, 'message' => $this->getMessage()], $this->status);
+    }
+}

+ 80 - 0
app/Http/Controllers/ProviderBankAccountController.php

@@ -0,0 +1,80 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Http\Requests\ProviderBankAccountStoreRequest;
+use App\Http\Requests\ProviderBankAccountUpdateRequest;
+use App\Http\Resources\ProviderBankAccountResource;
+use App\Models\Provider;
+use App\Services\ProviderBankAccountService;
+use Illuminate\Http\JsonResponse;
+use Illuminate\Support\Facades\Auth;
+
+class ProviderBankAccountController extends Controller
+{
+    public function __construct(
+        private readonly ProviderBankAccountService $service,
+    ) {}
+
+    public function index(): JsonResponse
+    {
+        $items = $this->service->getAll($this->resolveProvider());
+
+        return $this->successResponse(payload: ProviderBankAccountResource::collection($items));
+    }
+
+    public function store(ProviderBankAccountStoreRequest $request): JsonResponse
+    {
+        $item = $this->service->create($this->resolveProvider(), $request->validated());
+
+        return $this->successResponse(
+            payload: new ProviderBankAccountResource($item),
+            message: __('messages.bank_account_created'),
+            code:    201,
+        );
+    }
+
+    public function update(ProviderBankAccountUpdateRequest $request, int $id): JsonResponse
+    {
+        $item = $this->service->update($this->resolveProvider(), $id, $request->validated());
+
+        return $this->successResponse(
+            payload: new ProviderBankAccountResource($item),
+            message: __('messages.bank_account_updated'),
+        );
+    }
+
+    public function destroy(int $id): JsonResponse
+    {
+        $this->service->delete($this->resolveProvider(), $id);
+
+        return $this->successResponse(message: __('messages.bank_account_deleted'));
+    }
+
+    //
+
+    /**
+     * Somente o proprio prestador manipula as contas: o id nunca vem da URL.
+     */
+    public function forProvider(int $providerId): JsonResponse
+    {
+        $provider = Provider::findOrFail($providerId);
+
+        $items = $this->service->getAll($provider);
+
+        return $this->successResponse(payload: ProviderBankAccountResource::collection($items));
+    }
+
+    //
+
+    private function resolveProvider(): Provider
+    {
+        $provider = Auth::user()?->provider;
+
+        if (! $provider) {
+            abort(403, __('auth.unauthorized'));
+        }
+
+        return $provider;
+    }
+}

+ 0 - 22
app/Http/Controllers/ProviderController.php

@@ -2,7 +2,6 @@
 
 namespace App\Http\Controllers;
 
-use App\Http\Requests\ProviderBankAccountRequest;
 use App\Http\Requests\ProviderRequest;
 use App\Http\Requests\RegisterProviderRequest;
 use App\Http\Resources\AuthResource;
@@ -79,15 +78,6 @@ class ProviderController extends Controller
         ]);
     }
 
-    public function paymentMethods(int $id): JsonResponse
-    {
-        return $this->successResponse(
-            payload: $this->service->getPaymentMethods($id),
-        );
-    }
-
-    //
-
     public function approve(int $id): JsonResponse
     {
         $item = $this->service->approve($id);
@@ -127,18 +117,6 @@ class ProviderController extends Controller
         );
     }
 
-    public function updateBankAccount(ProviderBankAccountRequest $request, int $id): JsonResponse
-    {
-        $item = $this->service->updateBankAccount($id, $request->validated()['recipient_default_bank_account']);
-
-        return $this->successResponse(
-            payload: new ProviderResource($item),
-            message: __('messages.updated'),
-        );
-    }
-
-    //
-
     public function dailyPriceMin(): JsonResponse
     {
         return $this->successResponse(payload: [

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

@@ -65,13 +65,6 @@ class ProviderWithdrawalController extends Controller
         ]);
     }
 
-    public function configuration(): JsonResponse
-    {
-        return $this->successResponse(payload: [
-            'pix_enabled' => config('withdrawals.pix_enabled'),
-        ]);
-    }
-
     public function fees(): JsonResponse
     {
         return $this->successResponse(payload: $this->service->getWithdrawalFees());

+ 0 - 31
app/Http/Requests/ProviderBankAccountRequest.php

@@ -1,31 +0,0 @@
-<?php
-
-namespace App\Http\Requests;
-
-use Illuminate\Foundation\Http\FormRequest;
-use Illuminate\Validation\Rule;
-
-class ProviderBankAccountRequest extends FormRequest
-{
-    public function authorize(): bool
-    {
-        return true;
-    }
-
-    public function rules(): array
-    {
-        return [
-            'recipient_default_bank_account'                     => ['required', 'array'],
-            'recipient_default_bank_account.holder_name'         => ['sometimes', 'nullable', 'string', 'max:255'],
-            'recipient_default_bank_account.holder_type'         => ['sometimes', 'nullable', Rule::in(['individual', 'company'])],
-            'recipient_default_bank_account.holder_document'     => ['sometimes', 'nullable', 'string', 'max:20'],
-            'recipient_default_bank_account.bank'                => ['required', 'string', 'max:20'],
-            'recipient_default_bank_account.branch_number'       => ['required', 'string', 'max:20'],
-            'recipient_default_bank_account.branch_check_digit'  => ['sometimes', 'nullable', 'string', 'max:10'],
-            'recipient_default_bank_account.account_number'      => ['required', 'string', 'max:13'],
-            'recipient_default_bank_account.account_check_digit' => ['required', 'string', 'max:10'],
-            'recipient_default_bank_account.type'                => ['required', Rule::in(['checking', 'savings'])],
-            'recipient_default_bank_account.metadata'            => ['sometimes', 'array'],
-        ];
-    }
-}

+ 27 - 0
app/Http/Requests/ProviderBankAccountStoreRequest.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace App\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Validation\Rule;
+
+class ProviderBankAccountStoreRequest extends FormRequest
+{
+    public function authorize(): bool
+    {
+        return true;
+    }
+
+    public function rules(): array
+    {
+        return [
+            'bank_id'             => ['required_without:bank_code', 'nullable', 'integer', Rule::exists('banks', 'id')->where('active', true)->whereNull('deleted_at')],
+            'bank_code'           => ['required_without:bank_id', 'nullable', 'string', 'max:3'],
+            'type'                => ['required', Rule::in(['checking', 'savings'])],
+            'branch_number'       => ['required', 'string', 'max:4'],
+            'branch_check_digit'  => ['sometimes', 'nullable', 'string', 'max:2'],
+            'account_number'      => ['required', 'string', 'max:13'],
+            'account_check_digit' => ['required', 'string', 'max:2'],
+        ];
+    }
+}

+ 28 - 0
app/Http/Requests/ProviderBankAccountUpdateRequest.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace App\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Validation\Rule;
+
+class ProviderBankAccountUpdateRequest extends FormRequest
+{
+    public function authorize(): bool
+    {
+        return true;
+    }
+
+    public function rules(): array
+    {
+        return [
+            'bank_id'             => ['sometimes', 'nullable', 'integer', Rule::exists('banks', 'id')->where('active', true)->whereNull('deleted_at')],
+            'bank_code'           => ['sometimes', 'nullable', 'string', 'max:3'],
+            'type'                => ['sometimes', Rule::in(['checking', 'savings'])],
+            'branch_number'       => ['sometimes', 'string', 'max:4'],
+            'branch_check_digit'  => ['sometimes', 'nullable', 'string', 'max:2'],
+            'account_number'      => ['sometimes', 'string', 'max:13'],
+            'account_check_digit' => ['sometimes', 'string', 'max:2'],
+            'is_primary'          => ['sometimes', 'boolean'],
+        ];
+    }
+}

+ 0 - 51
app/Http/Requests/ProviderPaymentMethodRequest.php

@@ -1,51 +0,0 @@
-<?php
-
-namespace App\Http\Requests;
-
-use App\Enums\AccountTypeEnum;
-use App\Enums\BankAccountTypeEnum;
-use Illuminate\Foundation\Http\FormRequest;
-
-class ProviderPaymentMethodRequest extends FormRequest
-{
-    public function authorize(): bool
-    {
-        return true;
-    }
-
-    public function rules(): array
-    {
-        $pixAccountType   = AccountTypeEnum::PIX->value;
-        $bankAccountType  = AccountTypeEnum::BANK_ACCOUNT->value;
-        $accountTypes     = implode(',', [$pixAccountType, $bankAccountType]);
-        $bankAccountTypes = implode(',', [BankAccountTypeEnum::CHECKING->value, BankAccountTypeEnum::SAVINGS->value]);
-
-        if ($this->account_type == AccountTypeEnum::PIX->value) {
-
-            $rules =  [
-                'account_type'      => "sometimes|in:{$accountTypes}",
-                'pix_key'           => "required|string|max:255|required_if:account_type,{$pixAccountType}",
-                'bank_account_type' => "nullable|required_if:account_type,{$bankAccountType}|in:{$bankAccountTypes}",
-                'agency'            => "nullable|string|max:255|required_if:account_type,{$bankAccountType}",
-                'account'           => "nullable|string|max:255|required_if:account_type,{$bankAccountType}",
-                'digit'             => "nullable|string|max:255|required_if:account_type,{$bankAccountType}",
-            ];
-        } else {
-            $rules =  [
-                'account_type'      => "sometimes|in:{$accountTypes}",
-                'pix_key'           => "nullable|string|max:255|required_if:account_type,{$pixAccountType}",
-                'bank_account_type' => "sometimes|required_if:account_type,{$bankAccountType}|in:{$bankAccountTypes}",
-                'agency'            => "sometimes|string|max:255|required_if:account_type,{$bankAccountType}",
-                'account'           => "sometimes|string|max:255|required_if:account_type,{$bankAccountType}",
-                'digit'             => "sometimes|string|max:255|required_if:account_type,{$bankAccountType}",
-            ];
-        }
-
-        if ($this->isMethod('post')) {
-            $rules['provider_id'] = 'required|exists:providers,id';
-        };
-
-
-        return $rules;
-    }
-}

+ 4 - 4
app/Http/Requests/ProviderRequest.php

@@ -115,10 +115,10 @@ class ProviderRequest extends FormRequest
             $rules['recipient_default_bank_account.holder_type']         = ['required', Rule::in(['individual', 'company'])];
             $rules['recipient_default_bank_account.holder_document']     = 'required|string|max:20';
             $rules['recipient_default_bank_account.bank']                = 'required|string|max:3';
-            $rules['recipient_default_bank_account.branch_number']       = 'required|string|max:20';
-            $rules['recipient_default_bank_account.branch_check_digit']  = 'sometimes|nullable|string|max:10';
-            $rules['recipient_default_bank_account.account_number']      = 'required|string|max:20';
-            $rules['recipient_default_bank_account.account_check_digit'] = 'required|string|max:10';
+            $rules['recipient_default_bank_account.branch_number']       = 'required|string|max:4';
+            $rules['recipient_default_bank_account.branch_check_digit']  = 'sometimes|nullable|string|max:2';
+            $rules['recipient_default_bank_account.account_number']      = 'required|string|max:13';
+            $rules['recipient_default_bank_account.account_check_digit'] = 'required|string|max:2';
             $rules['recipient_default_bank_account.type']                = ['required', Rule::in(['checking', 'savings'])];
             $rules['recipient_default_bank_account.metadata']            = 'required|array';
             $rules['recipient_metadata']                                 = 'required|array';

+ 4 - 5
app/Http/Requests/RegisterProviderRequest.php

@@ -68,13 +68,12 @@ class RegisterProviderRequest extends FormRequest
       'recipient_default_bank_account.holder_document'     => 'sometimes|string|max:20',
       'recipient_default_bank_account.holder_type'         => 'sometimes|string|in:individual,company',
       'recipient_default_bank_account.bank'                => 'sometimes|string|max:3',
-      'recipient_default_bank_account.branch_number'       => 'sometimes|string|max:20',
-      'recipient_default_bank_account.branch_check_digit'  => 'sometimes|string|max:10',
-      'recipient_default_bank_account.account_number'      => 'sometimes|string|max:20',
-      'recipient_default_bank_account.account_check_digit' => 'sometimes|string|max:10',
+      'recipient_default_bank_account.branch_number'       => 'sometimes|string|max:4',
+      'recipient_default_bank_account.branch_check_digit'  => 'sometimes|string|nullable|max:2',
+      'recipient_default_bank_account.account_number'      => 'sometimes|string|max:13',
+      'recipient_default_bank_account.account_check_digit' => 'sometimes|string|max:2',
       'recipient_default_bank_account.type'                => 'sometimes|string|in:checking,savings',
       'recipient_default_bank_account.metadata'            => 'sometimes|array',
-      'recipient_default_bank_account.pix_key'             => 'sometimes|string|nullable|max:255',
 
       'recipient_metadata' => 'sometimes|array',
       

+ 46 - 0
app/Http/Resources/ProviderBankAccountResource.php

@@ -0,0 +1,46 @@
+<?php
+
+namespace App\Http\Resources;
+
+use App\Models\ProviderBankAccount;
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
+use Illuminate\Http\Resources\Json\JsonResource;
+
+class ProviderBankAccountResource extends JsonResource
+{
+    /**
+     * @return array<string, mixed>
+     */
+    public function toArray(Request $request): array
+    {
+        return [
+            'id'                  => $this->id,
+            'provider_id'         => $this->provider_id,
+            'bank_id'             => $this->bank_id,
+            'bank_code'           => $this->bank_code,
+            'bank_name'           => $this->bank?->name ?? $this->bank_name,
+            'holder_name'         => $this->holder_name,
+            'holder_document'     => $this->holder_document,
+            'type'                => $this->type,
+            'branch_number'       => $this->branch_number,
+            'branch_check_digit'  => $this->branch_check_digit,
+            'account_number'      => $this->account_number,
+            'account_check_digit' => $this->account_check_digit,
+            'is_primary'          => $this->is_primary,
+            'is_active'           => $this->is_active,
+            'synced'              => $this->isSyncedWithGateway(),
+            'created_at'          => $this->created_at?->format('d/m/Y H:i'),
+            'updated_at'          => $this->updated_at?->format('d/m/Y H:i'),
+        ];
+    }
+
+    /**
+     * @param \Illuminate\Database\Eloquent\Collection<ProviderBankAccount> $resource
+     * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection<ProviderBankAccountResource>
+     */
+    public static function collection($resource): AnonymousResourceCollection
+    {
+        return parent::collection($resource);
+    }
+}

+ 0 - 23
app/Http/Resources/ProviderPaymentMethodResource.php

@@ -1,23 +0,0 @@
-<?php
-
-namespace App\Http\Resources;
-
-use Illuminate\Http\Request;
-use Illuminate\Http\Resources\Json\JsonResource;
-
-class ProviderPaymentMethodResource extends JsonResource
-{
-    public function toArray(Request $request): array
-    {
-        return [
-            'id'                => $this->id,
-            'provider_id'       => $this->provider_id,
-            'account_type'      => $this->account_type?->value,
-            'pix_key'           => $this->pix_key,
-            'bank_account_type' => $this->bank_account_type?->value,
-            'agency'            => $this->agency,
-            'account'           => $this->account,
-            'digit'             => $this->digit,
-        ];
-    }
-}

+ 4 - 0
app/Http/Resources/ProviderResource.php

@@ -33,6 +33,10 @@ class ProviderResource extends JsonResource
             'profile_media_id'               => $this->profileMedia?->id,
             'recipient_id'                   => $this->recipient_id,
             'recipient_default_bank_account' => $this->recipient_default_bank_account,
+            'has_primary_bank_account'       => $this->relationLoaded('primaryBankAccount')
+                ? $this->primaryBankAccount !== null
+                : $this->primaryBankAccount()->exists(),
+            'bank_accounts'                  => ProviderBankAccountResource::collection($this->whenLoaded('bankAccounts')),
             'profile_media'                  => $this->profileMedia ? new MediaResource($this->profileMedia) : null,
             'document_front_media'           => $this->whenLoaded('documentFrontMedia', fn () => $this->documentFrontMedia ? new MediaResource($this->documentFrontMedia) : null),
             'document_back_media'            => $this->whenLoaded('documentBackMedia', fn () => $this->documentBackMedia ? new MediaResource($this->documentBackMedia) : null),

+ 33 - 4
app/Models/Provider.php

@@ -193,6 +193,21 @@ class Provider extends Model
 
     //
 
+    public function bankAccounts(): HasMany
+    {
+        return $this->hasMany(ProviderBankAccount::class)
+            ->orderBy('is_primary', 'desc')
+            ->orderBy('created_at', 'asc');
+    }
+
+    public function primaryBankAccount(): HasOne
+    {
+        return $this->hasOne(ProviderBankAccount::class)
+            ->where('is_primary', true);
+    }
+
+    //
+
     public function blockedByClients()
     {
         return $this->hasMany(ClientProviderBlock::class);
@@ -242,9 +257,23 @@ class Provider extends Model
 
     public function scopeVisibleToCustomers($query)
     {
-        return $query
-            ->whereNotNull('providers.recipient_default_bank_account')
-            ->whereRaw("providers.recipient_default_bank_account::text <> '{}'")
-            ->whereRaw("providers.recipient_default_bank_account::text <> '[]'");
+        return $query->whereExists(self::hasActivePrimaryBankAccount());
+    }
+
+    /**
+     * Prestador so aparece para os clientes quando tem conta bancaria principal ativa.
+     *
+     * Devolvido como closure para poder ser usado tambem em queries que chegam em
+     * providers por join (favoritos, propostas), e nao apenas pelo model.
+     */
+    public static function hasActivePrimaryBankAccount(): \Closure
+    {
+        return fn ($query) => $query
+            ->select(\Illuminate\Support\Facades\DB::raw(1))
+            ->from('provider_bank_accounts')
+            ->whereColumn('provider_bank_accounts.provider_id', 'providers.id')
+            ->where('provider_bank_accounts.is_primary', true)
+            ->where('provider_bank_accounts.is_active', true)
+            ->whereNull('provider_bank_accounts.deleted_at');
     }
 }

+ 151 - 0
app/Models/ProviderBankAccount.php

@@ -0,0 +1,151 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use Illuminate\Database\Eloquent\SoftDeletes;
+
+/**
+ * @property int $id
+ * @property int $provider_id
+ * @property int|null $bank_id
+ * @property string $bank_code
+ * @property string|null $bank_name
+ * @property string $holder_name
+ * @property string $holder_document
+ * @property string $holder_type
+ * @property string $type
+ * @property string $branch_number
+ * @property string|null $branch_check_digit
+ * @property string $account_number
+ * @property string $account_check_digit
+ * @property bool $is_primary
+ * @property bool $is_active
+ * @property \Illuminate\Support\Carbon|null $gateway_synced_at
+ * @property string|null $gateway_sync_hash
+ * @property array|null $gateway_payload
+ * @property \Illuminate\Support\Carbon|null $created_at
+ * @property \Illuminate\Support\Carbon|null $updated_at
+ * @property \Illuminate\Support\Carbon|null $deleted_at
+ * @property-read Bank|null $bank
+ * @property-read Provider $provider
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ProviderBankAccount newModelQuery()
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ProviderBankAccount newQuery()
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ProviderBankAccount onlyTrashed()
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ProviderBankAccount query()
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ProviderBankAccount active()
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ProviderBankAccount primary()
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ProviderBankAccount withTrashed(bool $withTrashed = true)
+ * @method static \Illuminate\Database\Eloquent\Builder<static>|ProviderBankAccount withoutTrashed()
+ * @mixin \Eloquent
+ */
+class ProviderBankAccount extends Model
+{
+    use HasFactory, SoftDeletes;
+
+    protected $table = 'provider_bank_accounts';
+
+    protected $fillable = [
+        'provider_id',
+        'bank_id',
+        'bank_code',
+        'bank_name',
+        'holder_name',
+        'holder_document',
+        'holder_type',
+        'type',
+        'branch_number',
+        'branch_check_digit',
+        'account_number',
+        'account_check_digit',
+        'is_primary',
+        'is_active',
+        'gateway_synced_at',
+        'gateway_sync_hash',
+        'gateway_sync_count',
+        'gateway_payload',
+    ];
+
+    protected function casts(): array
+    {
+        return [
+            'is_primary'         => 'boolean',
+            'is_active'          => 'boolean',
+            'gateway_synced_at'  => 'datetime',
+            'gateway_sync_count' => 'integer',
+            'gateway_payload'    => 'array',
+        ];
+    }
+
+    public function provider(): BelongsTo
+    {
+        return $this->belongsTo(Provider::class);
+    }
+
+    public function bank(): BelongsTo
+    {
+        return $this->belongsTo(Bank::class);
+    }
+
+    //
+
+    public function scopePrimary(Builder $query): Builder
+    {
+        return $query->where('is_primary', true);
+    }
+
+    public function scopeActive(Builder $query): Builder
+    {
+        return $query->where('is_active', true);
+    }
+
+    //
+
+    /**
+     * Formato esperado pelo Pagar.me em default_bank_account / bank_account.
+     * E tambem o formato espelhado em providers.recipient_default_bank_account.
+     */
+    public function toGatewayBankAccount(): array
+    {
+        return [
+            'holder_name'         => $this->holder_name,
+            'holder_type'         => $this->holder_type,
+            'holder_document'     => $this->holder_document,
+            'bank'                => $this->bank_code,
+            'branch_number'       => $this->branch_number,
+            'branch_check_digit'  => $this->branch_check_digit,
+            'account_number'      => $this->account_number,
+            'account_check_digit' => $this->account_check_digit,
+            'type'                => $this->type,
+        ];
+    }
+
+    /**
+     * Identifica o conteudo da conta enviado ao gateway, para detectar divergencia
+     * entre o que esta salvo aqui e o que o Pagar.me tem como conta padrao.
+     */
+    public function gatewayHash(): string
+    {
+        return sha1(json_encode($this->toGatewayBankAccount()));
+    }
+
+    /**
+     * Avanca o contador usado na chave de idempotencia, para que cada envio ao Pagar.me
+     * seja uma operacao nova - inclusive ao voltar para uma conta ja usada antes.
+     */
+    public function markGatewaySyncAttempt(): int
+    {
+        $this->forceFill(['gateway_sync_count' => $this->gateway_sync_count + 1])->save();
+
+        return $this->gateway_sync_count;
+    }
+
+    public function isSyncedWithGateway(): bool
+    {
+        return $this->gateway_synced_at !== null
+            && $this->gateway_sync_hash === $this->gatewayHash();
+    }
+}

+ 1 - 1
app/Services/AuthService.php

@@ -385,7 +385,7 @@ class AuthService
 
     public function createAppSession(User $user): array
     {
-        $user->loadMissing(['provider.profileMedia', 'client.profileMedia']);
+        $user->loadMissing(['provider.profileMedia', 'provider.primaryBankAccount', 'client.profileMedia']);
 
         $deviceId = Str::uuid()->toString();
 

+ 2 - 3
app/Services/ClientFavoriteProviderService.php

@@ -4,6 +4,7 @@ namespace App\Services;
 
 use App\Http\Resources\ClientFavoriteProviderResource;
 use App\Models\ClientFavoriteProvider;
+use App\Models\Provider;
 use Illuminate\Database\Eloquent\Collection;
 
 class ClientFavoriteProviderService
@@ -13,9 +14,7 @@ class ClientFavoriteProviderService
         return ClientFavoriteProvider::where('client_favorite_providers.client_id', $clientId)
             ->with('provider.profileMedia')
             ->leftJoin('providers', 'providers.id', '=', 'client_favorite_providers.provider_id')
-            ->whereNotNull('providers.recipient_default_bank_account')
-            ->whereRaw("providers.recipient_default_bank_account::text <> '{}'")
-            ->whereRaw("providers.recipient_default_bank_account::text <> '[]'")
+            ->whereExists(Provider::hasActivePrimaryBankAccount())
             ->leftJoin('users as provider_user', 'provider_user.id', '=', 'providers.user_id')
             ->leftJoin('addresses as provider_address', function ($join) {
                 $join->on('provider_address.source_id', '=', 'providers.id')

+ 2 - 6
app/Services/DashboardService.php

@@ -161,9 +161,7 @@ class DashboardService
 
         $favoriteProviders = ClientFavoriteProvider::where('client_favorite_providers.client_id', $cliente->id)
             ->leftJoin('providers', 'providers.id', '=', 'client_favorite_providers.provider_id')
-            ->whereNotNull('providers.recipient_default_bank_account')
-            ->whereRaw("providers.recipient_default_bank_account::text <> '{}'")
-            ->whereRaw("providers.recipient_default_bank_account::text <> '[]'")
+            ->whereExists(Provider::hasActivePrimaryBankAccount())
             ->leftJoin('users as provider_user', 'provider_user.id', '=', 'providers.user_id')
             ->leftJoinSub(
                 Address::preferredForProvider(),
@@ -418,9 +416,7 @@ class DashboardService
                 '=',
                 'providers.id'
             )
-            ->whereNotNull('providers.recipient_default_bank_account')
-            ->whereRaw("providers.recipient_default_bank_account::text <> '{}'")
-            ->whereRaw("providers.recipient_default_bank_account::text <> '[]'")
+            ->whereExists(Provider::hasActivePrimaryBankAccount())
             ->leftJoin('users', 'providers.user_id', '=', 'users.id')
             ->leftJoin(
                 DB::raw("

+ 51 - 2
app/Services/Pagarme/PagarmeRecipientService.php

@@ -13,6 +13,7 @@ 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\Models\ProviderBankAccount;
 use App\Services\Pagarme\Concerns\FormatsPagarmeData;
 use App\Services\Pagarme\Concerns\MocksPagarmeRequests;
 use App\Services\Pagarme\Concerns\SendsPagarmeRequests;
@@ -158,8 +159,37 @@ class PagarmeRecipientService
         return $recipientId;
     }
 
-    public function updateDefaultBankAccount(Provider $provider, array $bankAccountData): Provider
+    public function getRecipient(string $recipientId): array
     {
+        if ($this->shouldMockPagarmeRequest()) {
+            $provider = Provider::query()->where('recipient_id', $recipientId)->first();
+
+            return [
+                'id'                   => $recipientId,
+                'default_bank_account' => $provider?->recipient_default_bank_account ?? [],
+                'mocked'               => true,
+            ];
+        }
+
+        return $this->pagarmeRequest(
+            method:         'GET',
+            path:           "/recipients/{$recipientId}",
+            payload:        [],
+            idempotencyKey: "get-recipient-{$recipientId}",
+            errorMessage:   'Erro ao consultar recebedor no Pagar.me.',
+        );
+    }
+
+    /**
+     * O Pagar.me mantem UMA conta bancaria por recebedor. As demais contas do prestador
+     * existem apenas no nosso banco; aqui so trafega a conta principal.
+     *
+     * O recipient_id nao muda: o que e atualizado e o destino das transferencias (saques).
+     */
+    public function updateDefaultBankAccount(Provider $provider, ProviderBankAccount $bankAccount): Provider
+    {
+        $bankAccountData = $bankAccount->toGatewayBankAccount();
+
         $payload = new BankAccountUpdateRequestData(
             holderName:        $this->normalizeHolderName(data_get($bankAccountData, 'holder_name')),
             holderType:        data_get($bankAccountData, 'holder_type'),
@@ -184,7 +214,7 @@ class PagarmeRecipientService
             method:         'PATCH',
             path:           "/recipients/{$provider->recipient_id}/default-bank-account",
             payload:        $payload,
-            idempotencyKey: $this->idempotencyKey($provider, 'default-bank-account-'.sha1(json_encode($payload->toArray()))),
+            idempotencyKey: $this->idempotencyKey($provider, $this->bankAccountIdempotencySuffix($bankAccount)),
             errorMessage:   'Erro ao atualizar conta bancaria do recebedor no Pagar.me.',
         );
 
@@ -195,6 +225,25 @@ class PagarmeRecipientService
         return $provider->fresh();
     }
 
+    /**
+     * A chave precisa ser unica por operacao, e nao pelo conteudo da conta.
+     *
+     * Com o hash do payload, alternar a conta principal A -> B -> A reenviava a mesma chave
+     * da primeira troca: o Pagar.me tratava como repeticao, devolvia a resposta antiga e
+     * nao aplicava a volta para A - o saque continuaria caindo em B.
+     *
+     * Por isso o discriminador e um contador persistido, e nao o timestamp da conta:
+     * updated_at volta do banco truncado em segundos e repetiria a chave.
+     */
+    private function bankAccountIdempotencySuffix(ProviderBankAccount $bankAccount): string
+    {
+        return sprintf(
+            'default-bank-account-%d-%d',
+            $bankAccount->id,
+            $bankAccount->gateway_sync_count,
+        );
+    }
+
     //
 
     private function buildRecipientBankAccount(array $data): BankAccountData

+ 374 - 0
app/Services/ProviderBankAccountService.php

@@ -0,0 +1,374 @@
+<?php
+
+namespace App\Services;
+
+use App\Enums\ProviderWithdrawalStatusEnum;
+use App\Exceptions\BankAccountException;
+use App\Models\Bank;
+use App\Models\Provider;
+use App\Models\ProviderBankAccount;
+use App\Models\ProviderWithdrawal;
+use App\Services\Pagarme\PagarmeRecipientService;
+use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
+
+class ProviderBankAccountService
+{
+    public function __construct(
+        private readonly PagarmeRecipientService $pagarmeRecipientService,
+    ) {}
+
+    public function getAll(Provider $provider): Collection
+    {
+        return $provider->bankAccounts()->with('bank')->get();
+    }
+
+    public function findForProvider(Provider $provider, int $id): ProviderBankAccount
+    {
+        $account = ProviderBankAccount::query()
+            ->where('provider_id', $provider->id)
+            ->with('bank')
+            ->find($id);
+
+        if (! $account) {
+            throw new BankAccountException('bank_account_not_found', 404);
+        }
+
+        return $account;
+    }
+
+    //
+
+    public function create(Provider $provider, array $data): ProviderBankAccount
+    {
+        $attributes = $this->buildAttributes($provider, $data);
+
+        $this->guardAgainstDuplicate($provider, $attributes);
+
+        $isFirstAccount = ! $provider->bankAccounts()->exists();
+
+        return DB::transaction(function () use ($provider, $attributes, $isFirstAccount) {
+            $account = ProviderBankAccount::create([
+                ...$attributes,
+                'provider_id' => $provider->id,
+                'is_primary'  => $isFirstAccount,
+                'is_active'   => true,
+            ]);
+
+            if ($account->is_primary) {
+                $this->syncPrimaryToGateway($provider, $account);
+            }
+
+            return $account->fresh('bank');
+        });
+    }
+
+    /**
+     * Cadastro do prestador: a conta enviada no formulario nasce como principal.
+     *
+     * Aqui o recebedor e criado com os dados de endereco que vieram na propria requisicao
+     * (no cadastro o endereco pode ainda nao estar persistido).
+     */
+    public function createPrimaryFromRecipientPayload(Provider $provider, array $data): ?ProviderBankAccount
+    {
+        $bankAccount = data_get($data, 'recipient_default_bank_account', []);
+
+        $hasBankData = ! empty(data_get($bankAccount, 'bank')) && ! empty(data_get($bankAccount, 'account_number'));
+
+        if (! $hasBankData || $provider->bankAccounts()->exists()) {
+            return null;
+        }
+
+        $attributes = $this->buildAttributes($provider, [
+            'bank_code'           => data_get($bankAccount, 'bank'),
+            'type'                => data_get($bankAccount, 'type', 'checking'),
+            'branch_number'       => data_get($bankAccount, 'branch_number'),
+            'branch_check_digit'  => data_get($bankAccount, 'branch_check_digit'),
+            'account_number'      => data_get($bankAccount, 'account_number'),
+            'account_check_digit' => data_get($bankAccount, 'account_check_digit'),
+        ]);
+
+        $account = ProviderBankAccount::create([
+            ...$attributes,
+            'provider_id' => $provider->id,
+            'is_primary'  => true,
+            'is_active'   => true,
+        ]);
+
+        $account->markGatewaySyncAttempt();
+
+        if (empty($provider->recipient_id)) {
+            $this->pagarmeRecipientService->createRecipientForProvider($provider, [
+                ...$data,
+                'recipient_default_bank_account' => $account->toGatewayBankAccount(),
+            ]);
+        } else {
+            $this->pagarmeRecipientService->updateDefaultBankAccount($provider, $account);
+        }
+
+        $account->forceFill([
+            'gateway_synced_at' => now(),
+            'gateway_sync_hash' => $account->gatewayHash(),
+        ])->save();
+
+        return $account;
+    }
+
+    public function update(Provider $provider, int $id, array $data): ProviderBankAccount
+    {
+        $account = $this->findForProvider($provider, $id);
+
+        $attributes = $this->buildAttributes($provider, $data, $account);
+
+        $this->guardAgainstDuplicate($provider, $attributes, $account->id);
+
+        $makePrimary = ! $account->is_primary && filter_var(data_get($data, 'is_primary', false), FILTER_VALIDATE_BOOLEAN);
+
+        if ($makePrimary) {
+            if (! $account->is_active) {
+                throw new BankAccountException('bank_account_inactive');
+            }
+
+            $this->guardAgainstWithdrawalInProgress($provider);
+        }
+
+        return DB::transaction(function () use ($provider, $account, $attributes, $makePrimary) {
+            $account->fill($attributes)->save();
+
+            if ($account->is_primary) {
+                $this->syncPrimaryToGateway($provider, $account);
+            } elseif ($makePrimary) {
+                $this->promoteToPrimary($provider, $account);
+            }
+
+            return $account->fresh('bank');
+        });
+    }
+
+    public function delete(Provider $provider, int $id): bool
+    {
+        $account = $this->findForProvider($provider, $id);
+
+        $this->guardAgainstWithdrawalInProgress($provider);
+
+        if ($provider->bankAccounts()->count() <= 1) {
+            throw new BankAccountException('bank_account_last_cannot_be_deleted');
+        }
+
+        if ($account->is_primary) {
+            throw new BankAccountException('bank_account_primary_requires_replacement');
+        }
+
+        return (bool) $account->delete();
+    }
+
+    //
+
+    /**
+     * Confere, direto no Pagar.me, se a conta padrao do recebedor e mesmo a conta
+     * principal daqui - e reenvia se estiver divergente.
+     *
+     * A transacao do banco garante que nada seja gravado localmente sem o gateway ter
+     * aceitado, mas nao cobre o caminho inverso: o PATCH ser aplicado e o commit local
+     * falhar depois. Como quem decide o destino da transferencia e o gateway, essa
+     * conferencia roda antes de cada saque, que e o unico momento em que isso importa.
+     */
+    public function ensureGatewayMatchesPrimary(Provider $provider): ProviderBankAccount
+    {
+        $account = $provider->primaryBankAccount()->first();
+
+        if (! $account || ! $account->is_active) {
+            throw new BankAccountException('provider_requires_synced_bank_account');
+        }
+
+        try {
+            $remote = data_get(
+                $this->pagarmeRecipientService->getRecipient($provider->recipient_id),
+                'default_bank_account',
+                [],
+            );
+
+            if (! $this->matchesGatewayAccount($account, $remote)) {
+                DB::transaction(fn () => $this->syncPrimaryToGateway($provider, $account));
+            }
+        } catch (\Throwable $e) {
+            Log::error('Falha ao conferir a conta bancaria do recebedor no Pagar.me', [
+                'provider_id'             => $provider->id,
+                'provider_bank_account_id'=> $account->id,
+                'exception'               => $e->getMessage(),
+            ]);
+
+            throw new BankAccountException('provider_requires_synced_bank_account');
+        }
+
+        return $account->fresh();
+    }
+
+    private function matchesGatewayAccount(ProviderBankAccount $account, array $remote): bool
+    {
+        $local = $account->toGatewayBankAccount();
+
+        foreach (['bank', 'branch_number', 'account_number', 'account_check_digit', 'type'] as $field) {
+            if (ltrim((string) data_get($local, $field), '0') !== ltrim((string) data_get($remote, $field), '0')) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    //
+
+    /**
+     * O Pagar.me tem uma conta so por recebedor: promover uma conta significa
+     * desmarcar a anterior e enviar a nova como conta padrao das transferencias.
+     */
+    private function promoteToPrimary(Provider $provider, ProviderBankAccount $account): void
+    {
+        ProviderBankAccount::query()
+            ->where('provider_id', $provider->id)
+            ->where('id', '!=', $account->id)
+            ->where('is_primary', true)
+            ->update(['is_primary' => false]);
+
+        $account->forceFill(['is_primary' => true])->save();
+
+        $this->syncPrimaryToGateway($provider, $account);
+    }
+
+    /**
+     * Envia a conta principal ao Pagar.me. O recebedor continua sendo um so por prestador:
+     * o que muda e a conta padrao que recebe as transferencias (saques).
+     */
+    private function syncPrimaryToGateway(Provider $provider, ProviderBankAccount $account): void
+    {
+        $account->markGatewaySyncAttempt();
+
+        if (empty($provider->recipient_id)) {
+            $this->pagarmeRecipientService->createRecipientForProvider(
+                $provider,
+                $this->buildRecipientDataFromProvider($provider, $account),
+            );
+        } else {
+            $this->pagarmeRecipientService->updateDefaultBankAccount($provider, $account);
+        }
+
+        $account->forceFill([
+            'gateway_synced_at' => now(),
+            'gateway_sync_hash' => $account->gatewayHash(),
+        ])->save();
+    }
+
+    private function buildRecipientDataFromProvider(Provider $provider, ProviderBankAccount $account): array
+    {
+        $provider->loadMissing(['user', 'addresses.city', 'addresses.state']);
+
+        $address = $provider->addresses->first();
+
+        return [
+            'recipient_name'                 => $provider->user?->name,
+            'recipient_email'                => $provider->user?->email,
+            'recipient_document'             => $provider->document,
+            'recipient_type'                 => 'individual',
+            'recipient_payment_mode'         => 'bank_transfer',
+            'recipient_metadata'             => [],
+            'recipient_default_bank_account' => $account->toGatewayBankAccount(),
+            'birth_date'                     => $provider->birth_date,
+            'phone'                          => $provider->user?->phone,
+            'address'                        => $address?->address,
+            'number'                         => $address?->number,
+            'district'                       => $address?->district,
+            'complement'                     => $address?->complement,
+            'city'                           => $address?->city?->name,
+            'state'                          => $address?->state?->code,
+            'zip_code'                       => $address?->zip_code,
+        ];
+    }
+
+    //
+
+    /**
+     * O titular e sempre o proprio prestador: o Pagar.me exige que o documento da conta
+     * seja o mesmo documento do recebedor, entao nada disso vem do cliente.
+     */
+    private function buildAttributes(Provider $provider, array $data, ?ProviderBankAccount $current = null): array
+    {
+        $bank = $this->resolveBank($data, $current);
+
+        $provider->loadMissing('user');
+
+        return [
+            'bank_id'             => $bank->id,
+            'bank_code'           => $bank->code,
+            'bank_name'           => $bank->name,
+            'holder_name'         => trim((string) $provider->user?->name),
+            'holder_document'     => $this->digits($provider->document),
+            'holder_type'         => 'individual',
+            'type'                => data_get($data, 'type', $current?->type),
+            'branch_number'       => $this->digits(data_get($data, 'branch_number', $current?->branch_number)),
+            'branch_check_digit'  => $this->digits(data_get($data, 'branch_check_digit', $current?->branch_check_digit)) ?: null,
+            'account_number'      => $this->digits(data_get($data, 'account_number', $current?->account_number)),
+            'account_check_digit' => $this->digits(data_get($data, 'account_check_digit', $current?->account_check_digit)),
+        ];
+    }
+
+    private function resolveBank(array $data, ?ProviderBankAccount $current = null): Bank
+    {
+        $bankId   = data_get($data, 'bank_id');
+        $bankCode = data_get($data, 'bank_code');
+
+        if (! $bankId && ! $bankCode) {
+            $bankId   = $current?->bank_id;
+            $bankCode = $current?->bank_code;
+        }
+
+        $bank = Bank::query()
+            ->where('active', true)
+            ->when($bankId, fn ($query) => $query->where('id', $bankId))
+            ->when(! $bankId && $bankCode, fn ($query) => $query->where('code', str_pad((string) $bankCode, 3, '0', STR_PAD_LEFT)))
+            ->first();
+
+        if (! $bank) {
+            throw new BankAccountException('bank_not_found');
+        }
+
+        return $bank;
+    }
+
+    private function guardAgainstDuplicate(Provider $provider, array $attributes, ?int $ignoreId = null): void
+    {
+        $exists = ProviderBankAccount::query()
+            ->where('provider_id', $provider->id)
+            ->where('bank_code', data_get($attributes, 'bank_code'))
+            ->where('branch_number', data_get($attributes, 'branch_number'))
+            ->where('account_number', data_get($attributes, 'account_number'))
+            ->where('account_check_digit', data_get($attributes, 'account_check_digit'))
+            ->when($ignoreId, fn ($query) => $query->where('id', '!=', $ignoreId))
+            ->exists();
+
+        if ($exists) {
+            throw new BankAccountException('bank_account_already_registered');
+        }
+    }
+
+    private function guardAgainstWithdrawalInProgress(Provider $provider): void
+    {
+        $inProgress = ProviderWithdrawal::query()
+            ->where('provider_id', $provider->id)
+            ->whereIn('status', [
+                ProviderWithdrawalStatusEnum::PENDING_TRANSFER,
+                ProviderWithdrawalStatusEnum::PROCESSING,
+            ])
+            ->exists();
+
+        if ($inProgress) {
+            throw new BankAccountException('bank_account_blocked_by_withdrawal');
+        }
+    }
+
+    private function digits(?string $value): string
+    {
+        return preg_replace('/\D+/', '', (string) $value) ?? '';
+    }
+}

+ 7 - 81
app/Services/ProviderService.php

@@ -11,7 +11,6 @@ use App\Models\ProviderServicesType;
 use App\Models\ProviderWorkingDay;
 use App\Models\State;
 use App\Models\User;
-use App\Services\Pagarme\PagarmeRecipientService;
 use Illuminate\Database\Eloquent\Collection;
 use Illuminate\Http\UploadedFile;
 use Illuminate\Pagination\LengthAwarePaginator;
@@ -22,7 +21,7 @@ class ProviderService
 {
     public function __construct(
         private readonly AuthService $authService,
-        private readonly PagarmeRecipientService $pagarmeRecipientService,
+        private readonly ProviderBankAccountService $providerBankAccountService,
         private readonly MediaService $mediaService,
         private readonly EmailService $emailService,
     ) {}
@@ -49,8 +48,8 @@ class ProviderService
         return DB::transaction(function () use ($data) {
             $provider = Provider::create($data);
 
-            if (! empty(data_get($data, 'recipient_name')) && ! empty(data_get($data, 'recipient_default_bank_account'))) {
-                $this->pagarmeRecipientService->createRecipientForProvider($provider, $data);
+            if (! empty(data_get($data, 'recipient_name'))) {
+                $this->providerBankAccountService->createPrimaryFromRecipientPayload($provider, $data);
             }
 
             return $provider->fresh(['user', 'profileMedia']);
@@ -105,29 +104,6 @@ class ProviderService
 
     //
 
-    public function getPaymentMethods(int $id): array
-    {
-        $provider = Provider::find($id);
-
-        if (! $provider || empty($provider->recipient_default_bank_account)) {
-            return [];
-        }
-
-        $bankAccount = $provider->recipient_default_bank_account;
-
-        return [[
-            'id'                              => $provider->id,
-            'provider_id'                     => $provider->id,
-            'account_type'                    => 'bank_account',
-            'pix_key'                         => data_get($bankAccount, 'pix_key'),
-            'bank_account_type'               => data_get($bankAccount, 'type'),
-            'agency'                          => data_get($bankAccount, 'branch_number'),
-            'account'                         => data_get($bankAccount, 'account_number'),
-            'digit'                           => data_get($bankAccount, 'account_check_digit'),
-            'recipient_default_bank_account'  => $bankAccount,
-        ]];
-    }
-
     public function getPending(int $page = 1, int $perPage = 10): LengthAwarePaginator
     {
         return Provider::query()
@@ -239,18 +215,14 @@ class ProviderService
 
             $provider->save();
 
-            $bankAccount = data_get($data, 'recipient_default_bank_account', []);
-
-            $hasBankData = ! empty(data_get($bankAccount, 'bank')) && ! empty(data_get($bankAccount, 'account_number'));
-
-            if (! empty(data_get($data, 'recipient_name')) && $hasBankData && empty($provider->recipient_id)) {
-                $this->pagarmeRecipientService->createRecipientForProvider($provider, $data);
-            }
-
             Address::where('source', 'provider')->where('source_id', $provider->id)->delete();
 
             $this->createProviderAddress($provider->id, $data);
 
+            if (! empty(data_get($data, 'recipient_name'))) {
+                $this->providerBankAccountService->createPrimaryFromRecipientPayload($provider, $data);
+            }
+
             ProviderServicesType::where('provider_id', $provider->id)->delete();
 
             $this->createProviderServicesTypes($provider->id, $data);
@@ -286,30 +258,6 @@ class ProviderService
         }
     }
 
-    public function updateBankAccount(int $id, array $bankAccountData): ?Provider
-    {
-        $provider = Provider::with(['user', 'addresses.city', 'addresses.state'])->find($id);
-
-        if (! $provider) {
-            return null;
-        }
-
-        $bankAccountData['holder_name']     = data_get($bankAccountData, 'holder_name', $provider->user?->name);
-        $bankAccountData['holder_document'] = data_get($bankAccountData, 'holder_document', $provider->document);
-        $bankAccountData['holder_type']     = data_get($bankAccountData, 'holder_type', 'individual');
-
-        if (empty($provider->recipient_id)) {
-            $this->pagarmeRecipientService->createRecipientForProvider(
-                $provider,
-                $this->buildRecipientDataFromProvider($provider, $bankAccountData),
-            );
-        } else {
-            $this->pagarmeRecipientService->updateDefaultBankAccount($provider, $bankAccountData);
-        }
-
-        return $provider->fresh(['user', 'profileMedia']);
-    }
-
     //
 
     public function approve(int $id): Provider
@@ -347,28 +295,6 @@ class ProviderService
 
     //
 
-    private function buildRecipientDataFromProvider(Provider $provider, array $bankAccountData): array
-    {
-        $address = $provider->addresses->first();
-
-        return [
-            'recipient_name'                 => $provider->user?->name,
-            'recipient_email'                => $provider->user?->email,
-            'recipient_document'             => $provider->document,
-            'recipient_type'                 => 'individual',
-            'recipient_payment_mode'         => 'bank_transfer',
-            'recipient_metadata'             => [],
-            'recipient_default_bank_account' => $bankAccountData,
-            'birth_date'                     => $provider->birth_date,
-            'phone'                          => $provider->user?->phone,
-            'address'                        => $address?->address,
-            'number'                         => $address?->number,
-            'city'                           => $address?->city?->name,
-            'state'                          => $address?->state?->code,
-            'zip_code'                       => $address?->zip_code,
-        ];
-    }
-
     private function createProviderAddress(int $providerId, array $data): void
     {
         $state = null;

+ 6 - 2
app/Services/ProviderWithdrawalService.php

@@ -17,6 +17,7 @@ class ProviderWithdrawalService
 {
     public function __construct(
         private readonly PagarmeTransferService $pagarmeTransfer,
+        private readonly ProviderBankAccountService $providerBankAccounts,
     ) {}
 
     public function handleTransferWebhook(array $data): void
@@ -110,7 +111,9 @@ class ProviderWithdrawalService
             throw new \RuntimeException(__('messages.provider_requires_pagarme_recipient'));
         }
 
-        return DB::transaction(function () use ($provider) {
+        $primaryAccount = $this->providerBankAccounts->ensureGatewayMatchesPrimary($provider);
+
+        return DB::transaction(function () use ($provider, $primaryAccount) {
             Provider::query()
                 ->where('id', $provider->id)
                 ->lockForUpdate()
@@ -146,7 +149,7 @@ class ProviderWithdrawalService
 
             $idempotencyKey = sprintf('withdrawal-%d-%s', $provider->id, now()->timestamp);
 
-            $bankAccount = $provider->recipient_default_bank_account;
+            $bankAccount = $primaryAccount->toGatewayBankAccount();
 
             $withdrawal = ProviderWithdrawal::create([
                 'provider_id'        => $provider->id,
@@ -160,6 +163,7 @@ class ProviderWithdrawalService
 
                 'metadata' => [
                     'provider_id'                  => $provider->id,
+                    'provider_bank_account_id'     => $primaryAccount->id,
                     'gross_amount'                 => $available,
                     'payment_transfer_fee_amount'  => $paymentTransferFeeAmount,
                     'net_amount'                   => $netAmount,

+ 1 - 1
app/Services/UserService.php

@@ -82,7 +82,7 @@ class UserService
 
     public function me(): ?User
     {
-        return User::with(['provider.profileMedia', 'client.profileMedia'])->find(Auth::id());
+        return User::with(['provider.profileMedia', 'provider.primaryBankAccount', 'client.profileMedia'])->find(Auth::id());
     }
 
     public function updateMe(array $data): User

+ 0 - 5
config/withdrawals.php

@@ -1,5 +0,0 @@
-<?php
-
-return [
-    'pix_enabled' => (bool) env('PIX_WITHDRAWALS_ENABLED', false),
-];

+ 128 - 0
database/migrations/2026_08_12_100000_create_provider_bank_accounts_table.php

@@ -0,0 +1,128 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::create('provider_bank_accounts', function (Blueprint $table) {
+            $table->id();
+
+            $table->foreignId('provider_id')->constrained('providers')->cascadeOnDelete();
+            $table->foreignId('bank_id')->nullable()->constrained('banks')->nullOnDelete();
+
+            $table->string('bank_code', 3);
+            $table->string('bank_name')->nullable();
+
+            $table->string('holder_name');
+            $table->string('holder_document', 20);
+            $table->string('holder_type', 20)->default('individual');
+
+            $table->string('type', 20);
+            $table->string('branch_number', 4);
+            $table->string('branch_check_digit', 2)->nullable();
+            $table->string('account_number', 13);
+            $table->string('account_check_digit', 2);
+
+            $table->boolean('is_primary')->default(false);
+            $table->boolean('is_active')->default(true);
+
+            $table->timestamp('gateway_synced_at')->nullable();
+            $table->string('gateway_sync_hash')->nullable();
+            $table->unsignedInteger('gateway_sync_count')->default(0);
+            $table->json('gateway_payload')->nullable();
+
+            $table->timestamps();
+            $table->softDeletes();
+
+            $table->index('provider_id');
+        });
+
+        DB::statement('
+            CREATE UNIQUE INDEX provider_bank_accounts_primary_unique
+            ON provider_bank_accounts (provider_id)
+            WHERE is_primary = true AND deleted_at IS NULL
+        ');
+
+        DB::statement('
+            CREATE UNIQUE INDEX provider_bank_accounts_account_unique
+            ON provider_bank_accounts (provider_id, bank_code, branch_number, account_number, account_check_digit)
+            WHERE deleted_at IS NULL
+        ');
+
+        $this->backfillFromProviders();
+    }
+
+    public function down(): void
+    {
+        Schema::dropIfExists('provider_bank_accounts');
+    }
+
+    /**
+     * Migra a conta unica atual (providers.recipient_default_bank_account) para a nova tabela,
+     * marcando ela como principal. A coluna json continua sendo o padrão do que esta no Pagar.me.
+     * Criado apenas para evitar que o pessoal do Diária precise cadastrar as contas novamente no momento do deploy.
+     */
+    private function backfillFromProviders(): void
+    {
+        $banks = DB::table('banks')->pluck('id', 'code');
+
+        DB::table('providers')
+            ->whereNotNull('recipient_default_bank_account')
+            ->orderBy('id')
+            ->chunkById(100, function ($providers) use ($banks) {
+                foreach ($providers as $provider) {
+                    $account = json_decode($provider->recipient_default_bank_account ?? '', true);
+
+                    if (! is_array($account) || empty(data_get($account, 'bank')) || empty(data_get($account, 'account_number'))) {
+                        continue;
+                    }
+
+                    $bankCode = str_pad((string) data_get($account, 'bank'), 3, '0', STR_PAD_LEFT);
+
+                    $attributes = [
+                        'holder_name'         => data_get($account, 'holder_name') ?: '',
+                        'holder_document'     => data_get($account, 'holder_document') ?: '',
+                        'holder_type'         => data_get($account, 'holder_type') ?: 'individual',
+                        'type'                => data_get($account, 'type') ?: 'checking',
+                        'branch_number'       => data_get($account, 'branch_number') ?: '',
+                        'branch_check_digit'  => data_get($account, 'branch_check_digit'),
+                        'account_number'      => data_get($account, 'account_number'),
+                        'account_check_digit' => data_get($account, 'account_check_digit') ?: '',
+                    ];
+
+                    $gatewayHash = sha1(json_encode([
+                        'holder_name'         => $attributes['holder_name'],
+                        'holder_type'         => $attributes['holder_type'],
+                        'holder_document'     => $attributes['holder_document'],
+                        'bank'                => $bankCode,
+                        'branch_number'       => $attributes['branch_number'],
+                        'branch_check_digit'  => $attributes['branch_check_digit'],
+                        'account_number'      => $attributes['account_number'],
+                        'account_check_digit' => $attributes['account_check_digit'],
+                        'type'                => $attributes['type'],
+                    ]));
+
+                    DB::table('provider_bank_accounts')->insert([
+                        'provider_id'         => $provider->id,
+                        'bank_id'             => $banks[$bankCode] ?? null,
+                        'bank_code'           => $bankCode,
+                        'bank_name'           => null,
+                        ...$attributes,
+                        'is_primary'          => true,
+                        'is_active'           => true,
+                        'gateway_synced_at'   => $provider->recipient_id ? now() : null,
+                        'gateway_sync_hash'   => $provider->recipient_id ? $gatewayHash : null,
+                        'gateway_sync_count'  => $provider->recipient_id ? 1 : 0,
+                        'gateway_payload'     => null,
+                        'created_at'          => now(),
+                        'updated_at'          => now(),
+                    ]);
+                }
+            });
+    }
+};

+ 11 - 0
lang/en/messages.php

@@ -25,6 +25,17 @@ return [
     'schedule_requires_confirmed_provider'         => 'The appointment requires a confirmed provider before payment can be created.',
     'schedule_amount_must_be_positive'             => 'The appointment amount must be greater than zero before payment can be created.',
     'provider_requires_pagarme_recipient'          => 'The provider must have a Pagar.me recipient_id to receive the split.',
+    'bank_account_not_found'                       => 'Bank account not found.',
+    'bank_not_found'                               => 'Bank not found or inactive.',
+    'bank_account_already_registered'              => 'This bank account is already registered.',
+    'bank_account_inactive'                        => 'An inactive bank account cannot be used.',
+    'bank_account_primary_requires_replacement'    => 'Set another account as primary before deleting this one.',
+    'bank_account_last_cannot_be_deleted'          => 'You must keep at least one bank account registered.',
+    'bank_account_blocked_by_withdrawal'           => 'You have a withdrawal in progress. Wait until it completes to change your bank accounts.',
+    'bank_account_created'                         => 'Bank account registered successfully!',
+    'bank_account_updated'                         => 'Bank account updated successfully!',
+    'bank_account_deleted'                         => 'Bank account deleted successfully!',
+    'provider_requires_synced_bank_account'        => 'Your primary bank account is not synced. Review your bank details before withdrawing.',
     'schedule_payment_already_in_progress'         => 'A payment is already in progress for this appointment.',
     'payment_card_required'                        => 'A payment card or card_id is required.',
     'payment_card_not_found_or_inactive'           => 'The payment card was not found or is inactive for this client.',

+ 11 - 0
lang/es/messages.php

@@ -25,6 +25,17 @@ return [
     'schedule_requires_confirmed_provider'         => 'El servicio programado necesita un prestador confirmado para generar el pago.',
     'schedule_amount_must_be_positive'             => 'El valor del servicio programado debe ser mayor que cero para generar el pago.',
     'provider_requires_pagarme_recipient'          => 'El prestador debe tener un recipient_id de Pagar.me para recibir el split.',
+    'bank_account_not_found'                       => 'Cuenta bancaria no encontrada.',
+    'bank_not_found'                               => 'Banco no encontrado o inactivo.',
+    'bank_account_already_registered'              => 'Esta cuenta bancaria ya está registrada.',
+    'bank_account_inactive'                        => 'No se puede usar una cuenta bancaria inactiva.',
+    'bank_account_primary_requires_replacement'    => 'Define otra cuenta como principal antes de eliminar esta.',
+    'bank_account_last_cannot_be_deleted'          => 'Debes mantener al menos una cuenta bancaria registrada.',
+    'bank_account_blocked_by_withdrawal'           => 'Tienes un retiro en curso. Espera a que finalice para cambiar tus cuentas bancarias.',
+    'bank_account_created'                         => '¡Cuenta bancaria registrada con éxito!',
+    'bank_account_updated'                         => '¡Cuenta bancaria actualizada con éxito!',
+    'bank_account_deleted'                         => '¡Cuenta bancaria eliminada con éxito!',
+    'provider_requires_synced_bank_account'        => 'Tu cuenta bancaria principal no está sincronizada. Revisa tus datos bancarios antes de retirar.',
     'schedule_payment_already_in_progress'         => 'Ya existe un pago en curso para este servicio programado.',
     'payment_card_required'                        => 'La tarjeta de pago o el card_id es obligatorio.',
     'payment_card_not_found_or_inactive'           => 'La tarjeta de pago no fue encontrada o está inactiva para este cliente.',

+ 11 - 0
lang/pt/messages.php

@@ -25,6 +25,17 @@ return [
     'schedule_requires_confirmed_provider'         => 'O agendamento precisa ter um prestador confirmado para gerar o pagamento.',
     'schedule_amount_must_be_positive'             => 'O agendamento precisa ter valor maior que zero para gerar o pagamento.',
     'provider_requires_pagarme_recipient'          => 'O prestador precisa ter recipient_id do Pagar.me para receber o split.',
+    'bank_account_not_found'                       => 'Conta bancária não encontrada.',
+    'bank_not_found'                               => 'Banco não encontrado ou inativo.',
+    'bank_account_already_registered'              => 'Esta conta bancária já está cadastrada.',
+    'bank_account_inactive'                        => 'Não é possível usar uma conta bancária inativa.',
+    'bank_account_primary_requires_replacement'    => 'Defina outra conta como principal antes de excluir esta.',
+    'bank_account_last_cannot_be_deleted'          => 'Você precisa manter ao menos uma conta bancária cadastrada.',
+    'bank_account_blocked_by_withdrawal'           => 'Você possui um saque em andamento. Aguarde a conclusão para alterar suas contas bancárias.',
+    'bank_account_created'                         => 'Conta bancária cadastrada com sucesso!',
+    'bank_account_updated'                         => 'Conta bancária atualizada com sucesso!',
+    'bank_account_deleted'                         => 'Conta bancária excluída com sucesso!',
+    'provider_requires_synced_bank_account'        => 'Sua conta bancária principal não está sincronizada. Revise seus dados bancários antes de sacar.',
     'schedule_payment_already_in_progress'         => 'Já existe um pagamento em andamento para este agendamento.',
     'payment_card_required'                        => 'O cartão de pagamento ou card_id é obrigatório.',
     'payment_card_not_found_or_inactive'           => 'Cartão de pagamento não encontrado ou inativo para este cliente.',

+ 4 - 2
phpunit.xml

@@ -22,8 +22,10 @@
         <env name="APP_MAINTENANCE_DRIVER" value="file"/>
         <env name="BCRYPT_ROUNDS" value="4"/>
         <env name="CACHE_STORE" value="array"/>
-        <!-- <env name="DB_CONNECTION" value="sqlite"/> -->
-        <!-- <env name="DB_DATABASE" value=":memory:"/> -->
+        <env name="DB_CONNECTION" value="pgsql"/>
+        <env name="DB_DATABASE" value="diarista_test"/>
+        <env name="PAGARME_SECRET_KEY" value="sk_test_fake"/>
+        <env name="PAGARME_PLATFORM_RECIPIENT_ID" value="re_platform_fake"/>
         <env name="MAIL_MAILER" value="array"/>
         <env name="PULSE_ENABLED" value="false"/>
         <env name="QUEUE_CONNECTION" value="sync"/>

+ 3 - 6
routes/authRoutes/provider.php

@@ -11,14 +11,11 @@ Route::get('/provider/payment-splits',            [ProviderWithdrawalController:
 Route::get('/provider-withdrawals',               [ProviderWithdrawalController::class, 'all'])->middleware('permission:payment,view');
 Route::get('/provider/withdrawals/balance',       [ProviderWithdrawalController::class, 'balance'])->middleware('permission:config.provider,view');
 Route::get('/provider/withdrawals/fees',          [ProviderWithdrawalController::class, 'fees'])->middleware('permission:config.provider,view');
-Route::get('/provider/withdrawals/config',        [ProviderWithdrawalController::class, 'configuration']);
 Route::get('/provider/withdrawals',               [ProviderWithdrawalController::class, 'index'])->middleware('permission:config.provider,view');
 Route::post('/provider/withdrawals',              [ProviderWithdrawalController::class, 'store'])->middleware('permission:config.provider,edit');
 Route::get('/provider/withdrawals/{id}',          [ProviderWithdrawalController::class, 'show'])->middleware('permission:config.provider,view');
-Route::get('/provider/payment-methods/{id}',      [ProviderController::class, 'paymentMethods'])->middleware('permission:config.provider,view');
 Route::get('/provider',                           [ProviderController::class, 'index'])->middleware('permission:config.provider,view');
 Route::post('/provider',                          [ProviderController::class, 'store'])->middleware('permission:config.provider,add');
-Route::get('/provider/{id}',                      [ProviderController::class, 'show'])->middleware('permission:config.provider,view');
-Route::put('/provider/{id}',                      [ProviderController::class, 'update'])->middleware('permission:config.provider,edit');
-Route::patch('/provider/{id}/bank-account',       [ProviderController::class, 'updateBankAccount'])->middleware('permission:config.provider,edit');
-Route::delete('/provider/{id}',                   [ProviderController::class, 'destroy'])->middleware('permission:config.provider,delete');
+Route::get('/provider/{id}',                      [ProviderController::class, 'show'])->whereNumber('id')->middleware('permission:config.provider,view');
+Route::put('/provider/{id}',                      [ProviderController::class, 'update'])->whereNumber('id')->middleware('permission:config.provider,edit');
+Route::delete('/provider/{id}',                   [ProviderController::class, 'destroy'])->whereNumber('id')->middleware('permission:config.provider,delete');

+ 14 - 0
routes/authRoutes/provider_bank_account.php

@@ -0,0 +1,14 @@
+<?php
+
+use App\Http\Controllers\ProviderBankAccountController;
+use Illuminate\Support\Facades\Route;
+
+// contas do proprio prestador autenticado (o provider nunca vem da URL)
+Route::get('/provider/bank-accounts',                 [ProviderBankAccountController::class, 'index'])->middleware('permission:config.provider,view');
+Route::post('/provider/bank-accounts',                [ProviderBankAccountController::class, 'store'])->middleware('permission:config.provider,edit');
+// a troca da conta principal acontece na propria edicao da conta (campo is_primary)
+Route::put('/provider/bank-accounts/{id}',            [ProviderBankAccountController::class, 'update'])->middleware('permission:config.provider,edit');
+Route::delete('/provider/bank-accounts/{id}',         [ProviderBankAccountController::class, 'destroy'])->middleware('permission:config.provider,edit');
+
+// leitura pelo backoffice (admin nao altera dados bancarios)
+Route::get('/provider/{id}/bank-accounts',            [ProviderBankAccountController::class, 'forProvider'])->middleware('permission:config.provider,view');

+ 291 - 0
tests/Feature/ProviderBankAccountTest.php

@@ -0,0 +1,291 @@
+<?php
+
+namespace Tests\Feature;
+
+use App\Enums\ProviderWithdrawalStatusEnum;
+use App\Enums\UserTypeEnum;
+use App\Exceptions\BankAccountException;
+use App\Models\Bank;
+use App\Models\Provider;
+use App\Models\ProviderWithdrawal;
+use App\Models\User;
+use App\Services\ProviderBankAccountService;
+use App\Services\ProviderWithdrawalService;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Http\Client\Factory;
+use Illuminate\Support\Facades\Http;
+use Tests\TestCase;
+
+class ProviderBankAccountTest extends TestCase
+{
+    use RefreshDatabase;
+
+    private ProviderBankAccountService $service;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+
+        $this->fakePagarme();
+
+        $this->service = app(ProviderBankAccountService::class);
+
+        Bank::query()->create(['code' => '260', 'name' => 'Nu Pagamentos S.A.', 'active' => true]);
+        Bank::query()->create(['code' => '001', 'name' => 'Banco do Brasil S.A.', 'active' => true]);
+    }
+
+    /**
+     * Regressao: com a chave de idempotencia derivada do payload, voltar para a conta A
+     * reenviava a chave da primeira troca e o Pagar.me nao aplicava a alteracao.
+     */
+    public function test_alternar_conta_principal_a_b_a_usa_chaves_de_idempotencia_distintas(): void
+    {
+        $provider = $this->makeProvider();
+
+        $accountA = $this->service->create($provider, $this->accountData('260', '1111', '111111'));
+        $accountB = $this->service->create($provider, $this->accountData('001', '2222', '222222'));
+
+        $this->assertTrue($accountA->is_primary, 'a primeira conta cadastrada deve nascer principal');
+        $this->assertFalse($accountB->is_primary, 'a segunda conta nao pode virar principal sozinha');
+
+        $this->service->update($provider, $accountB->id, ['is_primary' => true]);
+        $this->service->update($provider, $accountA->id, ['is_primary' => true]);
+
+        $keys = $this->defaultBankAccountIdempotencyKeys();
+
+        $this->assertCount(3, $keys, 'cada troca/edicao da conta principal precisa chegar ao Pagar.me');
+        $this->assertCount(3, array_unique($keys), 'as chaves de idempotencia precisam ser distintas: '.implode(' | ', $keys));
+
+        $provider->refresh();
+
+        $this->assertSame('111111', data_get($provider->recipient_default_bank_account, 'account_number'));
+        $this->assertTrue($provider->primaryBankAccount()->first()->is($accountA));
+    }
+
+    public function test_conta_secundaria_nao_dispara_chamada_ao_pagarme(): void
+    {
+        $provider = $this->makeProvider();
+
+        $this->service->create($provider, $this->accountData('260', '1111', '111111'));
+
+        $callsAfterFirstAccount = count($this->defaultBankAccountIdempotencyKeys());
+
+        $secondary = $this->service->create($provider, $this->accountData('001', '2222', '222222'));
+
+        $this->service->update($provider, $secondary->id, ['branch_number' => '3333']);
+
+        $this->assertSame(
+            $callsAfterFirstAccount,
+            count($this->defaultBankAccountIdempotencyKeys()),
+            'criar ou editar conta nao principal e operacao local',
+        );
+    }
+
+    public function test_editar_conta_principal_ressincroniza_com_o_pagarme(): void
+    {
+        $provider = $this->makeProvider();
+
+        $account = $this->service->create($provider, $this->accountData('260', '1111', '111111'));
+
+        $before = count($this->defaultBankAccountIdempotencyKeys());
+
+        $updated = $this->service->update($provider, $account->id, ['account_number' => '999999']);
+
+        $this->assertCount($before + 1, $this->defaultBankAccountIdempotencyKeys());
+        $this->assertTrue($updated->isSyncedWithGateway());
+
+        $provider->refresh();
+
+        $this->assertSame('999999', data_get($provider->recipient_default_bank_account, 'account_number'));
+    }
+
+    public function test_marcar_como_principal_na_edicao_troca_a_conta_do_recebedor(): void
+    {
+        $provider = $this->makeProvider();
+
+        $accountA = $this->service->create($provider, $this->accountData('260', '1111', '111111'));
+        $accountB = $this->service->create($provider, $this->accountData('001', '2222', '222222'));
+
+        $updated = $this->service->update($provider, $accountB->id, [
+            'branch_number' => '3333',
+            'is_primary'    => true,
+        ]);
+
+        $this->assertTrue($updated->is_primary);
+        $this->assertFalse($accountA->fresh()->is_primary, 'a conta anterior precisa deixar de ser principal');
+
+        $provider->refresh();
+
+        $this->assertSame('222222', data_get($provider->recipient_default_bank_account, 'account_number'));
+        $this->assertSame('3333', data_get($provider->recipient_default_bank_account, 'branch_number'));
+    }
+
+    public function test_nao_permite_excluir_a_conta_principal(): void
+    {
+        $provider = $this->makeProvider();
+
+        $primary = $this->service->create($provider, $this->accountData('260', '1111', '111111'));
+
+        $this->service->create($provider, $this->accountData('001', '2222', '222222'));
+
+        $this->expectException(BankAccountException::class);
+        $this->expectExceptionMessage(__('messages.bank_account_primary_requires_replacement'));
+
+        $this->service->delete($provider, $primary->id);
+    }
+
+    public function test_nao_permite_excluir_a_ultima_conta(): void
+    {
+        $provider = $this->makeProvider();
+
+        $account = $this->service->create($provider, $this->accountData('260', '1111', '111111'));
+
+        $this->expectException(BankAccountException::class);
+        $this->expectExceptionMessage(__('messages.bank_account_last_cannot_be_deleted'));
+
+        $this->service->delete($provider, $account->id);
+    }
+
+    public function test_nao_permite_alterar_contas_com_saque_em_andamento(): void
+    {
+        $provider = $this->makeProvider();
+
+        $primary   = $this->service->create($provider, $this->accountData('260', '1111', '111111'));
+        $secondary = $this->service->create($provider, $this->accountData('001', '2222', '222222'));
+
+        ProviderWithdrawal::query()->create([
+            'provider_id'     => $provider->id,
+            'recipient_id'    => $provider->recipient_id,
+            'idempotency_key' => 'withdrawal-test-'.$provider->id,
+            'gross_amount'    => 100,
+            'net_amount'      => 96,
+            'status'          => ProviderWithdrawalStatusEnum::PROCESSING,
+        ]);
+
+        try {
+            $this->service->update($provider, $secondary->id, ['is_primary' => true]);
+
+            $this->fail('trocar a conta principal deveria ser bloqueado durante um saque');
+        } catch (BankAccountException $e) {
+            $this->assertSame(__('messages.bank_account_blocked_by_withdrawal'), $e->getMessage());
+        }
+
+        $this->assertTrue($provider->primaryBankAccount()->first()->is($primary));
+
+        $this->expectException(BankAccountException::class);
+        $this->expectExceptionMessage(__('messages.bank_account_blocked_by_withdrawal'));
+
+        $this->service->delete($provider, $secondary->id);
+    }
+
+    public function test_conta_divergente_no_gateway_e_reenviada_antes_do_saque(): void
+    {
+        $provider = $this->makeProvider();
+
+        $account = $this->service->create($provider, $this->accountData('260', '1111', '111111'));
+
+        // o recebedor esta com outra conta (ex: PATCH aplicado e commit local revertido)
+        $this->fakePagarme([
+            '*/recipients/re_*' => Http::response([
+                'id'                   => $provider->recipient_id,
+                'default_bank_account' => ['bank' => '001', 'branch_number' => '9999', 'account_number' => '888888', 'account_check_digit' => '7', 'type' => 'checking'],
+            ], 200),
+        ]);
+
+        $this->service->ensureGatewayMatchesPrimary($provider);
+
+        $this->assertCount(
+            1,
+            $this->defaultBankAccountIdempotencyKeys(),
+            'a conta principal local precisa ser reenviada quando o gateway diverge',
+        );
+
+        $this->assertTrue($account->fresh()->isSyncedWithGateway());
+    }
+
+    public function test_saque_bloqueado_quando_nao_e_possivel_sincronizar_a_conta(): void
+    {
+        $provider = $this->makeProvider();
+
+        $this->service->create($provider, $this->accountData('260', '1111', '111111'));
+
+        $this->fakePagarme([
+            '*/recipients/*/default-bank-account' => Http::response(['message' => 'erro'], 500),
+            '*/recipients/re_*'                   => Http::response([
+                'id'                   => $provider->recipient_id,
+                'default_bank_account' => ['bank' => '001', 'branch_number' => '9999', 'account_number' => '888888', 'account_check_digit' => '7', 'type' => 'checking'],
+            ], 200),
+        ]);
+
+        $this->expectException(BankAccountException::class);
+        $this->expectExceptionMessage(__('messages.provider_requires_synced_bank_account'));
+
+        app(ProviderWithdrawalService::class)->requestWithdrawal($provider->fresh());
+    }
+
+    //
+
+    /**
+     * Os stubs sao avaliados na ordem do array, entao os overrides vem primeiro.
+     * Atencao: cada chamada tambem zera as requisicoes ja gravadas.
+     */
+    private function fakePagarme(array $overrides = []): void
+    {
+        // Http::fake() acumula stubs e os antigos tem precedencia, entao a factory
+        // precisa ser descartada para um teste conseguir sobrescrever o stub do setUp
+        Http::clearResolvedInstances();
+        $this->app->forgetInstance(Factory::class);
+        Http::preventStrayRequests();
+
+        $defaults = [
+            '*/recipients/*/default-bank-account' => Http::response(['id' => 're_fake'], 200),
+            '*/recipients*'                       => Http::response(['id' => 're_fake'], 200),
+            '*'                                   => Http::response([], 200),
+        ];
+
+        // o "+" mantem os overrides na frente e sem colisao de chave com os defaults
+        Http::fake($overrides + array_diff_key($defaults, $overrides));
+    }
+
+    private function makeProvider(): Provider
+    {
+        $user = User::query()->create([
+            'name'     => 'Prestador Teste',
+            'email'    => 'prestador'.uniqid().'@teste.com',
+            'password' => 'secret',
+            'type'     => UserTypeEnum::PROVIDER->value,
+        ]);
+
+        return Provider::query()->create([
+            'user_id'      => $user->id,
+            'document'     => '06767310905',
+            'birth_date'   => '1990-01-01',
+            'recipient_id' => 're_'.uniqid(),
+        ]);
+    }
+
+    private function accountData(string $bankCode, string $branch, string $account): array
+    {
+        return [
+            'bank_code'           => $bankCode,
+            'type'                => 'checking',
+            'branch_number'       => $branch,
+            'branch_check_digit'  => '1',
+            'account_number'      => $account,
+            'account_check_digit' => '2',
+        ];
+    }
+
+    /**
+     * @return array<int, string>
+     */
+    private function defaultBankAccountIdempotencyKeys(): array
+    {
+        return Http::recorded()
+            ->filter(fn ($pair) => str_contains($pair[0]->url(), 'default-bank-account'))
+            ->map(fn ($pair) => $pair[0]->header('Idempotency-Key')[0] ?? '')
+            ->values()
+            ->all();
+    }
+
+}