ProviderBankAccountTest.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. <?php
  2. namespace Tests\Feature;
  3. use App\Enums\ProviderWithdrawalStatusEnum;
  4. use App\Enums\UserTypeEnum;
  5. use App\Exceptions\BankAccountException;
  6. use App\Models\Bank;
  7. use App\Models\Provider;
  8. use App\Models\ProviderWithdrawal;
  9. use App\Models\User;
  10. use App\Services\ProviderBankAccountService;
  11. use App\Services\ProviderWithdrawalService;
  12. use Illuminate\Foundation\Testing\RefreshDatabase;
  13. use Illuminate\Http\Client\Factory;
  14. use Illuminate\Support\Facades\Http;
  15. use Tests\TestCase;
  16. class ProviderBankAccountTest extends TestCase
  17. {
  18. use RefreshDatabase;
  19. private ProviderBankAccountService $service;
  20. protected function setUp(): void
  21. {
  22. parent::setUp();
  23. $this->fakePagarme();
  24. $this->service = app(ProviderBankAccountService::class);
  25. Bank::query()->create(['code' => '260', 'name' => 'Nu Pagamentos S.A.', 'active' => true]);
  26. Bank::query()->create(['code' => '001', 'name' => 'Banco do Brasil S.A.', 'active' => true]);
  27. }
  28. /**
  29. * Regressao: com a chave de idempotencia derivada do payload, voltar para a conta A
  30. * reenviava a chave da primeira troca e o Pagar.me nao aplicava a alteracao.
  31. */
  32. public function test_alternar_conta_principal_a_b_a_usa_chaves_de_idempotencia_distintas(): void
  33. {
  34. $provider = $this->makeProvider();
  35. $accountA = $this->service->create($provider, $this->accountData('260', '1111', '111111'));
  36. $accountB = $this->service->create($provider, $this->accountData('001', '2222', '222222'));
  37. $this->assertTrue($accountA->is_primary, 'a primeira conta cadastrada deve nascer principal');
  38. $this->assertFalse($accountB->is_primary, 'a segunda conta nao pode virar principal sozinha');
  39. $this->service->update($provider, $accountB->id, ['is_primary' => true]);
  40. $this->service->update($provider, $accountA->id, ['is_primary' => true]);
  41. $keys = $this->defaultBankAccountIdempotencyKeys();
  42. $this->assertCount(3, $keys, 'cada troca/edicao da conta principal precisa chegar ao Pagar.me');
  43. $this->assertCount(3, array_unique($keys), 'as chaves de idempotencia precisam ser distintas: '.implode(' | ', $keys));
  44. $provider->refresh();
  45. $this->assertSame('111111', data_get($provider->recipient_default_bank_account, 'account_number'));
  46. $this->assertTrue($provider->primaryBankAccount()->first()->is($accountA));
  47. }
  48. public function test_conta_secundaria_nao_dispara_chamada_ao_pagarme(): void
  49. {
  50. $provider = $this->makeProvider();
  51. $this->service->create($provider, $this->accountData('260', '1111', '111111'));
  52. $callsAfterFirstAccount = count($this->defaultBankAccountIdempotencyKeys());
  53. $secondary = $this->service->create($provider, $this->accountData('001', '2222', '222222'));
  54. $this->service->update($provider, $secondary->id, ['branch_number' => '3333']);
  55. $this->assertSame(
  56. $callsAfterFirstAccount,
  57. count($this->defaultBankAccountIdempotencyKeys()),
  58. 'criar ou editar conta nao principal e operacao local',
  59. );
  60. }
  61. public function test_editar_conta_principal_ressincroniza_com_o_pagarme(): void
  62. {
  63. $provider = $this->makeProvider();
  64. $account = $this->service->create($provider, $this->accountData('260', '1111', '111111'));
  65. $before = count($this->defaultBankAccountIdempotencyKeys());
  66. $updated = $this->service->update($provider, $account->id, ['account_number' => '999999']);
  67. $this->assertCount($before + 1, $this->defaultBankAccountIdempotencyKeys());
  68. $this->assertTrue($updated->isSyncedWithGateway());
  69. $provider->refresh();
  70. $this->assertSame('999999', data_get($provider->recipient_default_bank_account, 'account_number'));
  71. }
  72. public function test_marcar_como_principal_na_edicao_troca_a_conta_do_recebedor(): void
  73. {
  74. $provider = $this->makeProvider();
  75. $accountA = $this->service->create($provider, $this->accountData('260', '1111', '111111'));
  76. $accountB = $this->service->create($provider, $this->accountData('001', '2222', '222222'));
  77. $updated = $this->service->update($provider, $accountB->id, [
  78. 'branch_number' => '3333',
  79. 'is_primary' => true,
  80. ]);
  81. $this->assertTrue($updated->is_primary);
  82. $this->assertFalse($accountA->fresh()->is_primary, 'a conta anterior precisa deixar de ser principal');
  83. $provider->refresh();
  84. $this->assertSame('222222', data_get($provider->recipient_default_bank_account, 'account_number'));
  85. $this->assertSame('3333', data_get($provider->recipient_default_bank_account, 'branch_number'));
  86. }
  87. public function test_nao_permite_excluir_a_conta_principal(): void
  88. {
  89. $provider = $this->makeProvider();
  90. $primary = $this->service->create($provider, $this->accountData('260', '1111', '111111'));
  91. $this->service->create($provider, $this->accountData('001', '2222', '222222'));
  92. $this->expectException(BankAccountException::class);
  93. $this->expectExceptionMessage(__('messages.bank_account_primary_requires_replacement'));
  94. $this->service->delete($provider, $primary->id);
  95. }
  96. public function test_nao_permite_excluir_a_ultima_conta(): void
  97. {
  98. $provider = $this->makeProvider();
  99. $account = $this->service->create($provider, $this->accountData('260', '1111', '111111'));
  100. $this->expectException(BankAccountException::class);
  101. $this->expectExceptionMessage(__('messages.bank_account_last_cannot_be_deleted'));
  102. $this->service->delete($provider, $account->id);
  103. }
  104. public function test_nao_permite_alterar_contas_com_saque_em_andamento(): void
  105. {
  106. $provider = $this->makeProvider();
  107. $primary = $this->service->create($provider, $this->accountData('260', '1111', '111111'));
  108. $secondary = $this->service->create($provider, $this->accountData('001', '2222', '222222'));
  109. ProviderWithdrawal::query()->create([
  110. 'provider_id' => $provider->id,
  111. 'recipient_id' => $provider->recipient_id,
  112. 'idempotency_key' => 'withdrawal-test-'.$provider->id,
  113. 'gross_amount' => 100,
  114. 'net_amount' => 96,
  115. 'status' => ProviderWithdrawalStatusEnum::PROCESSING,
  116. ]);
  117. try {
  118. $this->service->update($provider, $secondary->id, ['is_primary' => true]);
  119. $this->fail('trocar a conta principal deveria ser bloqueado durante um saque');
  120. } catch (BankAccountException $e) {
  121. $this->assertSame(__('messages.bank_account_blocked_by_withdrawal'), $e->getMessage());
  122. }
  123. $this->assertTrue($provider->primaryBankAccount()->first()->is($primary));
  124. $this->expectException(BankAccountException::class);
  125. $this->expectExceptionMessage(__('messages.bank_account_blocked_by_withdrawal'));
  126. $this->service->delete($provider, $secondary->id);
  127. }
  128. public function test_conta_divergente_no_gateway_e_reenviada_antes_do_saque(): void
  129. {
  130. $provider = $this->makeProvider();
  131. $account = $this->service->create($provider, $this->accountData('260', '1111', '111111'));
  132. // o recebedor esta com outra conta (ex: PATCH aplicado e commit local revertido)
  133. $this->fakePagarme([
  134. '*/recipients/re_*' => Http::response([
  135. 'id' => $provider->recipient_id,
  136. 'default_bank_account' => ['bank' => '001', 'branch_number' => '9999', 'account_number' => '888888', 'account_check_digit' => '7', 'type' => 'checking'],
  137. ], 200),
  138. ]);
  139. $this->service->ensureGatewayMatchesPrimary($provider);
  140. $this->assertCount(
  141. 1,
  142. $this->defaultBankAccountIdempotencyKeys(),
  143. 'a conta principal local precisa ser reenviada quando o gateway diverge',
  144. );
  145. $this->assertTrue($account->fresh()->isSyncedWithGateway());
  146. }
  147. public function test_saque_bloqueado_quando_nao_e_possivel_sincronizar_a_conta(): void
  148. {
  149. $provider = $this->makeProvider();
  150. $this->service->create($provider, $this->accountData('260', '1111', '111111'));
  151. $this->fakePagarme([
  152. '*/recipients/*/default-bank-account' => Http::response(['message' => 'erro'], 500),
  153. '*/recipients/re_*' => Http::response([
  154. 'id' => $provider->recipient_id,
  155. 'default_bank_account' => ['bank' => '001', 'branch_number' => '9999', 'account_number' => '888888', 'account_check_digit' => '7', 'type' => 'checking'],
  156. ], 200),
  157. ]);
  158. $this->expectException(BankAccountException::class);
  159. $this->expectExceptionMessage(__('messages.provider_requires_synced_bank_account'));
  160. app(ProviderWithdrawalService::class)->requestWithdrawal($provider->fresh());
  161. }
  162. //
  163. /**
  164. * Os stubs sao avaliados na ordem do array, entao os overrides vem primeiro.
  165. * Atencao: cada chamada tambem zera as requisicoes ja gravadas.
  166. */
  167. private function fakePagarme(array $overrides = []): void
  168. {
  169. // Http::fake() acumula stubs e os antigos tem precedencia, entao a factory
  170. // precisa ser descartada para um teste conseguir sobrescrever o stub do setUp
  171. Http::clearResolvedInstances();
  172. $this->app->forgetInstance(Factory::class);
  173. Http::preventStrayRequests();
  174. $defaults = [
  175. '*/recipients/*/default-bank-account' => Http::response(['id' => 're_fake'], 200),
  176. '*/recipients*' => Http::response(['id' => 're_fake'], 200),
  177. '*' => Http::response([], 200),
  178. ];
  179. // o "+" mantem os overrides na frente e sem colisao de chave com os defaults
  180. Http::fake($overrides + array_diff_key($defaults, $overrides));
  181. }
  182. private function makeProvider(): Provider
  183. {
  184. $user = User::query()->create([
  185. 'name' => 'Prestador Teste',
  186. 'email' => 'prestador'.uniqid().'@teste.com',
  187. 'password' => 'secret',
  188. 'type' => UserTypeEnum::PROVIDER->value,
  189. ]);
  190. return Provider::query()->create([
  191. 'user_id' => $user->id,
  192. 'document' => '06767310905',
  193. 'birth_date' => '1990-01-01',
  194. 'recipient_id' => 're_'.uniqid(),
  195. ]);
  196. }
  197. private function accountData(string $bankCode, string $branch, string $account): array
  198. {
  199. return [
  200. 'bank_code' => $bankCode,
  201. 'type' => 'checking',
  202. 'branch_number' => $branch,
  203. 'branch_check_digit' => '1',
  204. 'account_number' => $account,
  205. 'account_check_digit' => '2',
  206. ];
  207. }
  208. /**
  209. * @return array<int, string>
  210. */
  211. private function defaultBankAccountIdempotencyKeys(): array
  212. {
  213. return Http::recorded()
  214. ->filter(fn ($pair) => str_contains($pair[0]->url(), 'default-bank-account'))
  215. ->map(fn ($pair) => $pair[0]->header('Idempotency-Key')[0] ?? '')
  216. ->values()
  217. ->all();
  218. }
  219. }