From 67f83283227870bad917eefa221b870c05f6dd89 Mon Sep 17 00:00:00 2001 From: Lucas Rosa Date: Wed, 21 Oct 2026 14:42:31 -0300 Subject: [PATCH] =?UTF-8?q?fix:=20decide=20entre=20CPF=20e=20CNPJ=20pela?= =?UTF-8?q?=20quantidade=20de=20d=C3=ADgitos=20do=20documento?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - adiciona Util\DocumentUtil::cpfCnpj(): com 11 dígitos o documento é sempre CPF; nos outros tamanhos mantém o comportamento anterior - usa o DocumentUtil em Customer (cpf_cnpj e tax_receipt), Seller (cpf_cnpj) e Holder (cpfCnpj), que enviavam como CNPJ todo CPF cujo valor completado com zeros também passava na validação de CNPJ - cobre com testes de DocumentUtil, dos models e da request do CustomerEndpoint::update - registra a v1.4.2 no CHANGELOG e a regra no docs/sdk.md - documenta a validação contra o ambiente local no docs/setup.md e, no skill, o chown do vendor/ e do .phpunit.cache --- .../skills/ipag-sdk-php-execution/SKILL.md | 18 +++++- CHANGELOG.md | 10 ++++ docs/sdk.md | 6 ++ docs/setup.md | 13 +++++ src/Model/Customer.php | 5 +- src/Model/Holder.php | 3 +- src/Model/Seller.php | 3 +- src/Util/DocumentUtil.php | 30 ++++++++++ tests/DocumentUtilTest.php | 58 +++++++++++++++++++ tests/Endpoint/CustomerEndpointTest.php | 22 +++++++ tests/Model/CustomerTest.php | 32 ++++++++++ tests/Model/HolderTest.php | 13 +++++ tests/Model/SellerTest.php | 13 +++++ 13 files changed, 219 insertions(+), 7 deletions(-) create mode 100644 src/Util/DocumentUtil.php create mode 100644 tests/DocumentUtilTest.php diff --git a/.claude/skills/ipag-sdk-php-execution/SKILL.md b/.claude/skills/ipag-sdk-php-execution/SKILL.md index b821d37..b907dfe 100644 --- a/.claude/skills/ipag-sdk-php-execution/SKILL.md +++ b/.claude/skills/ipag-sdk-php-execution/SKILL.md @@ -98,7 +98,15 @@ docker run --rm -u "$(id -u):$(id -g)" -e COMPOSER_HOME=/tmp/composer \ ``` Se o `vendor/` estiver atrás do lock, ou com dono `root`, reinstale antes de -confiar em qualquer resultado de teste. +confiar em qualquer resultado de teste. Máquinas que já usaram o compose local +costumam ter `vendor/` e `.phpunit.cache/` com dono `root`. O cache gera um +warning de permissão no PHPUnit em 8.x. Devolva os dois ao seu usuário antes do +`install`: + +```bash +docker run --rm -v "$PWD":/app composer:2 \ + chown -R "$(id -u):$(id -g)" /app/vendor /app/.phpunit.cache +``` --- @@ -232,8 +240,12 @@ O que os testes precisam cobrir: **Validação contra a API** (quando a task espera comportamento da API, ex. "a API retorna o documento com a máscara X"): -1. Rode um exemplo de `examples/` contra a **sandbox**, com credenciais - editadas localmente em `examples/config.php`. +1. Rode um exemplo de `examples/` contra a **sandbox**, ou contra o ambiente + local se o usuário indicar, com credenciais editadas localmente em + `examples/config.php`. O `LOCAL` só resolve com o container na rede da API + ([setup.md § Exemplos](../../../docs/setup.md#exemplos)). Para comparar + vários casos, use um script avulso no scratchpad que faça `require` do + `examples/config.php`, sem editar os exemplos versionados. 2. Desfaça a edição antes de qualquer `git add` (`git diff --quiet examples/config.php`). 3. Sem credenciais de sandbox, diga isso explicitamente e deixe a validação pendente. Não a dê como feita com base só no teste unitário. diff --git a/CHANGELOG.md b/CHANGELOG.md index 38ebb2a..a02c7fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,3 +5,13 @@ Todas as mudanças notáveis neste projeto serão documentadas neste arquivo. O formato é baseado em [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), e este projeto adere ao [Versionamento Semântico](https://semver.org/spec/v2.0.0.html). As versões até `v1.4.1` não têm entrada aqui: consulte as [tags](https://github.com/ipagdevs/ipag-sdk-php/tags) e o histórico de commits. + +## v1.4.2 - 2026-10-21 + +### Corrigido + +- `Customer` (`cpf_cnpj` e `tax_receipt`), `Seller` (`cpf_cnpj`) e `Holder` (`cpfCnpj`) não transformam mais um CPF em CNPJ. Um documento com 11 dígitos, com ou sem máscara, passa a ser sempre tratado como CPF e enviado com 11 dígitos. Antes, quando o CPF completado com zeros até 14 dígitos também era um CNPJ válido (todo CPF iniciado em `00` e uma parte dos demais), o SDK enviava esse CNPJ, e a API cadastrava o cliente como pessoa jurídica. + - **O valor enviado muda** para esses CPFs: `003.344.769-17` passa a ser enviado como `00334476917`, e não mais como `00000334476917`. + - Um CNPJ informado com 11 dígitos (sem os três primeiros zeros) deixa de ser completado e passa a ser validado como CPF. Envie o CNPJ com os 14 dígitos. + - Documentos com outros tamanhos seguem com o comportamento anterior. Um exemplo é o CPF passado como `int`, que perde os zeros à esquerda. Para não depender disso, envie o CPF como string de 11 dígitos. + - A atualização não corrige clientes já cadastrados com o CNPJ errado: segundo a documentação da API, o `cpf_cnpj` não pode ser alterado depois da criação do cliente. diff --git a/docs/sdk.md b/docs/sdk.md index 75901bc..181b9a6 100644 --- a/docs/sdk.md +++ b/docs/sdk.md @@ -104,6 +104,12 @@ protected function schema(SchemaBuilder $schema): Schema `$ctx->assert($cond, 'msg')`, que lançam `MutatorAttributeException`. Para validar, o código costuma usar `Kubinyete\Assertation\Assert`. `Model::loadMutator` descobre o mutator com `is_callable([$this, $nome])`. +- **Documento que aceita CPF ou CNPJ** (`Customer::cpf_cnpj` e `tax_receipt`, + `Seller::cpf_cnpj`, `Holder::cpfCnpj`) passa por + `Util\DocumentUtil::cpfCnpj()`, e com 11 dígitos é sempre CPF. Não volte para + `asCpf(false)->or()->asCnpj(false)`. O `asCnpj` completa o valor com zeros + até 14 dígitos, e o `get()` prefere o último ramo válido, então todo CPF + iniciado em `00` (e uma parte dos outros) vira CNPJ. - **Serialização:** `jsonSerialize()` emite **todo** atributo não-`hidden`, **inclusive `null`**, e serializa as relações recursivamente. - Os models v2 (`PaymentLinkV2`, `SubscriptionV2`, `Plan`, `PlanTrial`) diff --git a/docs/setup.md b/docs/setup.md index f337594..c350d3d 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -76,6 +76,19 @@ Para rodar um exemplo: Alguns exemplos usam IDs fixos (ex. `examples/subscription_v2/02-*.php`) que só existem na conta de quem os escreveu. Troque pelos da sua conta. +**Ambiente local.** `IpagEnvironment::LOCAL` (`api.ipag.test`, sem esquema) é o +nome de serviço da API dentro da rede do docker compose do ambiente local, e +não resolve no host. Para usar esse ambiente, rode o container do PHP na mesma +rede da API: + +```bash +docker run --rm --network -u "$(id -u):$(id -g)" -v "$PWD":/app -w /app \ + php:7.4-cli php examples/customer/00-customer-create.php +``` + +Para descobrir a rede, rode `docker inspect `. Para conferir, +`getent hosts api.ipag.test` dentro do container precisa devolver o IP da API. + ## CI `.github/workflows/ci-tests.yml` é o único workflow. diff --git a/src/Model/Customer.php b/src/Model/Customer.php index c5134c0..6ee7b18 100644 --- a/src/Model/Customer.php +++ b/src/Model/Customer.php @@ -4,6 +4,7 @@ use Ipag\Sdk\Model\Schema\Schema; use Kubinyete\Assertation\Assert; +use Ipag\Sdk\Util\DocumentUtil; use Ipag\Sdk\Model\Schema\Mutator; use Ipag\Sdk\Model\Schema\SchemaBuilder; @@ -255,7 +256,7 @@ protected function cpf_cnpj(): Mutator fn($value, $ctx) => is_null($value) ? $value : - Assert::value($value)->asCpf(false)->or()->asCnpj(false)->get() ?? $ctx->raise('inválido') + DocumentUtil::cpfCnpj($value) ?? $ctx->raise('inválido') ); } @@ -266,7 +267,7 @@ protected function tax_receipt(): Mutator fn($value, $ctx) => is_null($value) ? $value : - Assert::value($value)->asCpf(false)->or()->asCnpj(false)->get() ?? $ctx->raise('inválido') + DocumentUtil::cpfCnpj($value) ?? $ctx->raise('inválido') ); } diff --git a/src/Model/Holder.php b/src/Model/Holder.php index 88013f7..10be108 100644 --- a/src/Model/Holder.php +++ b/src/Model/Holder.php @@ -6,6 +6,7 @@ use Ipag\Sdk\Model\Schema\Schema; use Ipag\Sdk\Model\Schema\SchemaBuilder; use Kubinyete\Assertation\Assert; +use Ipag\Sdk\Util\DocumentUtil; /** * Holder Class @@ -78,7 +79,7 @@ protected function cpfCnpj(): Mutator fn($value, $ctx) => is_null($value) ? $value : - Assert::value($value)->asCpf(false)->or()->asCnpj(false)->get() ?? $ctx->raise('inválido') + DocumentUtil::cpfCnpj($value) ?? $ctx->raise('inválido') ); } diff --git a/src/Model/Seller.php b/src/Model/Seller.php index b1fb916..758e420 100644 --- a/src/Model/Seller.php +++ b/src/Model/Seller.php @@ -6,6 +6,7 @@ use Ipag\Sdk\Model\Schema\Schema; use Ipag\Sdk\Model\Schema\SchemaBuilder; use Kubinyete\Assertation\Assert; +use Ipag\Sdk\Util\DocumentUtil; /** * Seller Class @@ -83,7 +84,7 @@ protected function cpf_cnpj(): Mutator fn($value, $ctx) => is_null($value) ? $value : - Assert::value($value)->asCpf(false)->or()->asCnpj(false)->get() ?? $ctx->raise('inválido') + DocumentUtil::cpfCnpj($value) ?? $ctx->raise('inválido') ); } diff --git a/src/Util/DocumentUtil.php b/src/Util/DocumentUtil.php new file mode 100644 index 0000000..469fe24 --- /dev/null +++ b/src/Util/DocumentUtil.php @@ -0,0 +1,30 @@ +asCpf(false); + + if (strlen(preg_replace('/[^0-9]/', '', strval($document))) !== 11) { + $assert = $assert->or()->asCnpj(false); + } + + return $assert->get(); + } +} diff --git a/tests/DocumentUtilTest.php b/tests/DocumentUtilTest.php new file mode 100644 index 0000000..e5d034e --- /dev/null +++ b/tests/DocumentUtilTest.php @@ -0,0 +1,58 @@ +assertSame($expected, DocumentUtil::cpfCnpj($document)); + } + + /** + * @dataProvider invalidCpfCnpjProvider + */ + public function testShouldReturnNullForAnInvalidCpfCnpj($document) + { + $this->assertNull(DocumentUtil::cpfCnpj($document)); + } + + public function validCpfCnpjProvider(): array + { + return [ + // completados com zeros até 14 dígitos, estes CPFs também são CNPJs válidos + 'cpf iniciado em 00' => ['00334476917', '00334476917'], + 'cpf iniciado em 00 com máscara' => ['003.344.769-17', '00334476917'], + 'cpf iniciado em um único 0' => ['04654733930', '04654733930'], + 'cpf sem zero à esquerda' => ['74032971307', '74032971307'], + + 'cpf sem colisão' => ['60863754295', '60863754295'], + 'cnpj iniciado em 0' => ['01234567000195', '01234567000195'], + 'cnpj com máscara' => ['01.234.567/0001-95', '01234567000195'], + 'cnpj iniciado em 000' => ['00012345678030', '00012345678030'], + + // fora de 11 e 14 dígitos, o valor continua sendo completado com zeros + 'cnpj sem o zero à esquerda' => ['1234567000195', '01234567000195'], + 'cpf sem o zero à esquerda' => ['4114856790', '04114856790'], + 'cpf como int' => [4114856790, '04114856790'], + ]; + } + + public function invalidCpfCnpjProvider(): array + { + return [ + 'cpf com dv inválido' => ['074.598.263-80'], + 'cpf iniciado em 00 com dv inválido' => ['00334476918'], + // com 11 dígitos é CPF, mesmo que completado com zeros vire o CNPJ válido 00012345678030 + 'cnpj sem os três zeros à esquerda' => ['12345678030'], + 'cnpj com dv inválido' => ['01234567000196'], + 'dígitos repetidos' => ['11111111111'], + 'vazio' => [''], + ]; + } +} diff --git a/tests/Endpoint/CustomerEndpointTest.php b/tests/Endpoint/CustomerEndpointTest.php index 9b4634a..1e63306 100644 --- a/tests/Endpoint/CustomerEndpointTest.php +++ b/tests/Endpoint/CustomerEndpointTest.php @@ -2,13 +2,35 @@ namespace Ipag\Sdk\Tests\Endpoint; +use GuzzleHttp\Handler\MockHandler; +use GuzzleHttp\HandlerStack; +use GuzzleHttp\Middleware; use GuzzleHttp\Psr7\Response; +use Ipag\Sdk\Core\IpagEnvironment; use Ipag\Sdk\Exception\HttpException; +use Ipag\Sdk\Mock\IpagClientMock; use Ipag\Sdk\Model\Customer; use Ipag\Sdk\Tests\IpagClient; class CustomerEndpointTest extends IpagClient { + public function testShouldSendAnElevenDigitCpfAsCpfOnUpdate() + { + $history = []; + $stack = HandlerStack::create(new MockHandler([new Response(200, [], '{}')])); + $stack->push(Middleware::history($history)); + $client = new IpagClientMock('test', 'AAA', IpagEnvironment::SANDBOX, '2', ['handler' => $stack]); + + // completado com zeros até 14 dígitos, um CPF iniciado em 00 também é um CNPJ válido + $client->customer()->update(new Customer(['cpf_cnpj' => '003.344.769-17']), 10); + + $request = $history[0]['request']; + $this->assertSame('PUT', $request->getMethod()); + $this->assertSame('/service/resources/customers', $request->getUri()->getPath()); + $this->assertSame('id=10', $request->getUri()->getQuery()); + $this->assertSame('00334476917', json_decode((string) $request->getBody(), true)['cpf_cnpj']); + } + public function testShouldResponseSuccess() { $this->instanceClient([ diff --git a/tests/Model/CustomerTest.php b/tests/Model/CustomerTest.php index 35176d9..d5c96ac 100644 --- a/tests/Model/CustomerTest.php +++ b/tests/Model/CustomerTest.php @@ -225,6 +225,38 @@ public function testShouldThrowAValidationExceptionOnTheCustomerTaxReceiptProper $customer->setTaxReceipt('074.598.263-80'); } + public function testShouldKeepAnElevenDigitCpfOnTheCustomerCpfCnpjAndTaxReceiptProperties() + { + // completado com zeros até 14 dígitos, um CPF iniciado em 00 também é um CNPJ válido + $customer = new \Ipag\Sdk\Model\Customer([ + 'cpf_cnpj' => '003.344.769-17', + 'tax_receipt' => '00334476917', + ]); + + $this->assertEquals('00334476917', $customer->getCpfCnpj()); + $this->assertEquals('00334476917', $customer->getTaxReceipt()); + $this->assertEquals('00334476917', $customer->jsonSerialize()['cpf_cnpj']); + $this->assertEquals('00334476917', $customer->jsonSerialize()['tax_receipt']); + + $customer + ->setCpfCnpj('74032971307') + ->setTaxReceipt('04654733930'); + + $this->assertEquals('74032971307', $customer->getCpfCnpj()); + $this->assertEquals('04654733930', $customer->getTaxReceipt()); + } + + public function testShouldKeepAFourteenDigitCnpjOnTheCustomerCpfCnpjAndTaxReceiptProperties() + { + $customer = new \Ipag\Sdk\Model\Customer([ + 'cpf_cnpj' => '01.234.567/0001-95', + 'tax_receipt' => '01234567000195', + ]); + + $this->assertEquals('01234567000195', $customer->getCpfCnpj()); + $this->assertEquals('01234567000195', $customer->getTaxReceipt()); + } + public function testShouldThrowAValidationExceptionOnTheCustomerBirthdateProperty() { $customer = new \Ipag\Sdk\Model\Customer(); diff --git a/tests/Model/HolderTest.php b/tests/Model/HolderTest.php index 59278b0..ef00de6 100644 --- a/tests/Model/HolderTest.php +++ b/tests/Model/HolderTest.php @@ -96,6 +96,19 @@ public function testShouldThrowAValidationExceptionOnTheHolderCpfCnpjProperty() $holder->setCpfCnpj('074.598.263-80'); } + public function testShouldKeepAnElevenDigitCpfOnTheHolderCpfCnpjProperty() + { + // completado com zeros até 14 dígitos, um CPF iniciado em 00 também é um CNPJ válido + $holder = new \Ipag\Sdk\Model\Holder(['cpfCnpj' => '003.344.769-17']); + + $this->assertEquals('00334476917', $holder->getCpfCnpj()); + $this->assertEquals('00334476917', $holder->jsonSerialize()['cpfCnpj']); + + $holder->setCpfCnpj('01.234.567/0001-95'); + + $this->assertEquals('01234567000195', $holder->getCpfCnpj()); + } + public function testShouldThrowAValidationExceptionOnTheHolderPhoneProperty() { $holder = new \Ipag\Sdk\Model\Holder(); diff --git a/tests/Model/SellerTest.php b/tests/Model/SellerTest.php index f41da4b..bfcce33 100644 --- a/tests/Model/SellerTest.php +++ b/tests/Model/SellerTest.php @@ -172,6 +172,19 @@ public function testShouldThrowAValidationExceptionOnTheSellerCpfCnpjProperty() $seller->setCpfCnpj('074.598.263-80'); } + public function testShouldKeepAnElevenDigitCpfOnTheSellerCpfCnpjProperty() + { + // completado com zeros até 14 dígitos, um CPF iniciado em 00 também é um CNPJ válido + $seller = new \Ipag\Sdk\Model\Seller(['cpf_cnpj' => '003.344.769-17']); + + $this->assertEquals('00334476917', $seller->getCpfCnpj()); + $this->assertEquals('00334476917', $seller->jsonSerialize()['cpf_cnpj']); + + $seller->setCpfCnpj('01.234.567/0001-95'); + + $this->assertEquals('01234567000195', $seller->getCpfCnpj()); + } + public function testShouldThrowAValidationExceptionOnTheSellerEmailProperty() { $seller = new \Ipag\Sdk\Model\Seller();