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