| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374 |
- <?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) ?? '';
- }
- }
|