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 */ 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(); } }