From 25e425a60f0807a5ad921f1b194773ff631fcea9 Mon Sep 17 00:00:00 2001 From: Clintonrocha98 Date: Thu, 30 Oct 2025 13:51:37 -0300 Subject: [PATCH 1/7] feat: update CreatePixQrCodeRequest and builder to handle optional fields and improve error handling --- .../Builder/CreatePixQrCodeRequestBuilder.php | 40 ++++------------ .../Http/Request/CreatePixQrCodeRequest.php | 46 ++++++++++++------- 2 files changed, 38 insertions(+), 48 deletions(-) diff --git a/src/Pix/Http/Builder/CreatePixQrCodeRequestBuilder.php b/src/Pix/Http/Builder/CreatePixQrCodeRequestBuilder.php index 8200060..3f195c7 100644 --- a/src/Pix/Http/Builder/CreatePixQrCodeRequestBuilder.php +++ b/src/Pix/Http/Builder/CreatePixQrCodeRequestBuilder.php @@ -28,28 +28,28 @@ public function amount(int $amount): self return $this; } - public function expiresIn(int $seconds): self + public function expiresIn(?int $seconds): self { $this->expiresIn = $seconds; return $this; } - public function description(string $description): self + public function description(?string $description): self { $this->description = $description; return $this; } - public function customer(PixCustomerRequest $customer): self + public function customer(?PixCustomerRequest $customer): self { $this->customer = $customer; return $this; } - public function metadata(PixMetadataRequest $metadata): self + public function metadata(?PixMetadataRequest $metadata): self { $this->metadata = $metadata; @@ -61,38 +61,16 @@ public function metadata(PixMetadataRequest $metadata): self */ public function build(): CreatePixQrCodeRequest { - $errors = []; - if ($this->amount === null) { - $errors[] = 'amount'; - } - - if ($this->expiresIn === null) { - $errors[] = 'expiresIn'; - } - - if ($this->description === null) { - $errors[] = 'description'; - } - - if (! $this->customer instanceof PixCustomerRequest) { - $errors[] = 'customer'; - } - - if (! $this->metadata instanceof PixMetadataRequest) { - $errors[] = 'metadata'; - } - - if ($errors !== []) { - throw AbacatePayException::missingRequiredFields($errors); + throw AbacatePayException::missingRequiredFields(['amount']); } return new CreatePixQrCodeRequest( amount: $this->amount, - expiresIn: $this->expiresIn, - description: $this->description, - customer: $this->customer, - metadata: $this->metadata, + expiresIn: $this->expiresIn ?? 0, + description: $this->description ?? '', + customer: $this->customer ?? new PixCustomerRequest('', '', '', ''), + metadata: $this->metadata ?? new PixMetadataRequest(''), ); } } diff --git a/src/Pix/Http/Request/CreatePixQrCodeRequest.php b/src/Pix/Http/Request/CreatePixQrCodeRequest.php index 96b3754..922c841 100644 --- a/src/Pix/Http/Request/CreatePixQrCodeRequest.php +++ b/src/Pix/Http/Request/CreatePixQrCodeRequest.php @@ -7,14 +7,14 @@ use Basement\AbacatePay\Pix\Http\Builder\CreatePixQrCodeRequestBuilder; use JsonSerializable; -final readonly class CreatePixQrCodeRequest implements JsonSerializable +final readonly class CreatePixQrCodeRequest { public function __construct( public int $amount, - public int $expiresIn, - public string $description, - public PixCustomerRequest $customer, - public PixMetadataRequest $metadata, + public ?int $expiresIn, + public ?string $description, + public ?PixCustomerRequest $customer, + public ?PixMetadataRequest $metadata, ) {} public static function builder(): CreatePixQrCodeRequestBuilder @@ -26,21 +26,33 @@ public static function make(array $data): self { return new self( amount: $data['amount'], - expiresIn: $data['expiresIn'], - description: $data['description'], - customer: $data['customer'], - metadata: $data['metadata'], + expiresIn: $data['expiresIn'] ?? null, + description: $data['description'] ?? null, + customer: $data['customer'] ?? null, + metadata: $data['metadata'] ?? null, ); } - public function jsonSerialize(): array + public function toArray(): array { - return [ - 'amount' => $this->amount, - 'expiresIn' => $this->expiresIn, - 'description' => $this->description, - 'customer' => $this->customer->toArray(), - 'metadata' => $this->metadata->toArray(), - ]; + $data = ['amount' => $this->amount]; + + if ($this->expiresIn !== null) { + $data['expiresIn'] = $this->expiresIn; + } + + if ($this->description !== null) { + $data['description'] = $this->description; + } + + if ($this->customer instanceof PixCustomerRequest) { + $data['customer'] = $this->customer->toArray(); + } + + if ($this->metadata instanceof PixMetadataRequest) { + $data['metadata'] = $this->metadata->toArray(); + } + + return $data; } } From 8a6b1e75b6fd83f44160c01cae250f0e5185c467 Mon Sep 17 00:00:00 2001 From: Clintonrocha98 Date: Thu, 30 Oct 2025 13:51:26 -0300 Subject: [PATCH 2/7] feat: implement PixBuilder with tests for CreatePixQrCodeRequest and exception handling --- tests/Unit/Pix/PixBuilder.php | 63 +++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 tests/Unit/Pix/PixBuilder.php diff --git a/tests/Unit/Pix/PixBuilder.php b/tests/Unit/Pix/PixBuilder.php new file mode 100644 index 0000000..a67b988 --- /dev/null +++ b/tests/Unit/Pix/PixBuilder.php @@ -0,0 +1,63 @@ +name('Maria') + ->cellphone('+5511999999999') + ->email('maria@email.com') + ->taxId('12345678900') + ->build(); + + $metadata = PixMetadataRequest::builder() + ->externalId('order-123') + ->build(); + + $request = CreatePixQrCodeRequest::builder() + ->amount(10000) + ->expiresIn(3600) + ->description('Pagamento de assinatura') + ->customer($customer) + ->metadata($metadata) + ->build(); + + expect($request) + ->toBeInstanceOf(CreatePixQrCodeRequest::class) + ->and($request->amount)->toBe(10000) + ->and($request->description)->toBe('Pagamento de assinatura') + ->and($request->customer->name)->toBe('Maria') + ->and($request->metadata->externalId)->toBe('order-123') + ->and($customer->toArray())->toBe([ + 'name' => 'Maria', + 'cellphone' => '+5511999999999', + 'email' => 'maria@email.com', + 'taxId' => '12345678900', + ]) + ->and($metadata->toArray())->toBe([ + 'externalId' => 'order-123', + ]) + ->and($request->toArray())->toBe([ + 'amount' => 10000, + 'expiresIn' => 3600, + 'description' => 'Pagamento de assinatura', + 'customer' => [ + 'name' => 'Maria', + 'cellphone' => '+5511999999999', + 'email' => 'maria@email.com', + 'taxId' => '12345678900', + ], + 'metadata' => [ + 'externalId' => 'order-123', + ], + ]); +}); + +it('throws AbacatePayException when missing required fields', function () { + CreatePixQrCodeRequest::builder()->build(); +})->throws(AbacatePayException::class, 'Missing required fields'); From a449ecbfe7afcd3bbe2055cd25e8389740afd41f Mon Sep 17 00:00:00 2001 From: Clintonrocha98 Date: Thu, 30 Oct 2025 13:51:17 -0300 Subject: [PATCH 3/7] refactor: update PixQrCodeEntity constructor and modify PixResource to use toArray method for request serialization --- src/Pix/Entities/PixQrCodeEntity.php | 5 +---- src/Pix/PixResource.php | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Pix/Entities/PixQrCodeEntity.php b/src/Pix/Entities/PixQrCodeEntity.php index e94cae6..36c55a9 100644 --- a/src/Pix/Entities/PixQrCodeEntity.php +++ b/src/Pix/Entities/PixQrCodeEntity.php @@ -4,15 +4,12 @@ namespace Basement\AbacatePay\Pix\Entities; -use Basement\AbacatePay\Billing\Enum\BillingMethodEnum; use Basement\AbacatePay\Billing\Enum\BillingStatusEnum; use JsonSerializable; final readonly class PixQrCodeEntity implements JsonSerializable { - /** - * @param BillingMethodEnum[] $methods - */ + public function __construct( public string $id, public int $amount, diff --git a/src/Pix/PixResource.php b/src/Pix/PixResource.php index 25f2783..201cb3b 100644 --- a/src/Pix/PixResource.php +++ b/src/Pix/PixResource.php @@ -29,7 +29,7 @@ public function createQrCode(CreatePixQrCodeRequest $request): CreatePixQrCodeRe { try { $response = $this->client->post(sprintf('%s/create', self::BASE_PATH), [ - 'json' => $request->jsonSerialize(), + 'json' => $request->toArray(), ]); $responsePayload = json_decode( From 83fb6866456a6a11ca94dd9f671d9c6253719ec7 Mon Sep 17 00:00:00 2001 From: Clintonrocha98 Date: Thu, 30 Oct 2025 14:57:20 -0300 Subject: [PATCH 4/7] refactor: replace jsonSerialize with toArray method in CreateCouponRequest and update related classes --- src/Coupon/CouponResource.php | 2 +- .../Http/Request/CreateCouponRequest.php | 5 +-- src/Pix/Entities/PixQrCodeEntity.php | 1 - .../Builder/CreatePixQrCodeRequestBuilder.php | 8 ++-- .../Http/Request/CreatePixQrCodeRequest.php | 1 - tests/Feature/Pix/PixResourceTest.php | 39 ++++++++++--------- tests/Unit/Pix/PixBuilder.php | 18 +++++++++ 7 files changed, 45 insertions(+), 29 deletions(-) diff --git a/src/Coupon/CouponResource.php b/src/Coupon/CouponResource.php index 5a721b3..a38327c 100644 --- a/src/Coupon/CouponResource.php +++ b/src/Coupon/CouponResource.php @@ -28,7 +28,7 @@ public function create(CreateCouponRequest $request): CouponResponse { try { $response = $this->client->post(sprintf('%s/create', self::BASE_PATH), [ - 'json' => $request->jsonSerialize(), + 'json' => $request->toArray(), ]); $responsePayload = json_decode( diff --git a/src/Coupon/Http/Request/CreateCouponRequest.php b/src/Coupon/Http/Request/CreateCouponRequest.php index 1944872..f765943 100644 --- a/src/Coupon/Http/Request/CreateCouponRequest.php +++ b/src/Coupon/Http/Request/CreateCouponRequest.php @@ -6,9 +6,8 @@ use Basement\AbacatePay\Coupon\Enums\CouponDiscountKindEnum; use Basement\AbacatePay\Coupon\Http\Builder\CreateCouponRequestBuilder; -use JsonSerializable; -final readonly class CreateCouponRequest implements JsonSerializable +final readonly class CreateCouponRequest { public function __construct( public string $code, @@ -36,7 +35,7 @@ public static function builder(): CreateCouponRequestBuilder return new CreateCouponRequestBuilder; } - public function jsonSerialize(): array + public function toArray(): array { return [ 'code' => $this->code, diff --git a/src/Pix/Entities/PixQrCodeEntity.php b/src/Pix/Entities/PixQrCodeEntity.php index 36c55a9..4b84185 100644 --- a/src/Pix/Entities/PixQrCodeEntity.php +++ b/src/Pix/Entities/PixQrCodeEntity.php @@ -9,7 +9,6 @@ final readonly class PixQrCodeEntity implements JsonSerializable { - public function __construct( public string $id, public int $amount, diff --git a/src/Pix/Http/Builder/CreatePixQrCodeRequestBuilder.php b/src/Pix/Http/Builder/CreatePixQrCodeRequestBuilder.php index 3f195c7..d247fc6 100644 --- a/src/Pix/Http/Builder/CreatePixQrCodeRequestBuilder.php +++ b/src/Pix/Http/Builder/CreatePixQrCodeRequestBuilder.php @@ -67,10 +67,10 @@ public function build(): CreatePixQrCodeRequest return new CreatePixQrCodeRequest( amount: $this->amount, - expiresIn: $this->expiresIn ?? 0, - description: $this->description ?? '', - customer: $this->customer ?? new PixCustomerRequest('', '', '', ''), - metadata: $this->metadata ?? new PixMetadataRequest(''), + expiresIn: $this->expiresIn ?? null, + description: $this->description ?? null, + customer: $this->customer, + metadata: $this->metadata, ); } } diff --git a/src/Pix/Http/Request/CreatePixQrCodeRequest.php b/src/Pix/Http/Request/CreatePixQrCodeRequest.php index 922c841..15b5f13 100644 --- a/src/Pix/Http/Request/CreatePixQrCodeRequest.php +++ b/src/Pix/Http/Request/CreatePixQrCodeRequest.php @@ -5,7 +5,6 @@ namespace Basement\AbacatePay\Pix\Http\Request; use Basement\AbacatePay\Pix\Http\Builder\CreatePixQrCodeRequestBuilder; -use JsonSerializable; final readonly class CreatePixQrCodeRequest { diff --git a/tests/Feature/Pix/PixResourceTest.php b/tests/Feature/Pix/PixResourceTest.php index 07481c3..c81b721 100644 --- a/tests/Feature/Pix/PixResourceTest.php +++ b/tests/Feature/Pix/PixResourceTest.php @@ -16,20 +16,24 @@ use GuzzleHttp\Psr7\Response; beforeEach(function () { - $this->requestDto = CreatePixQrCodeRequest::make([ - 'amount' => 100, - 'expiresIn' => 10, - 'description' => 'abacate description', - 'customer' => PixCustomerRequest::make([ - 'name' => 'customer name', - 'cellphone' => '11982516627', - 'email' => 'joe@doe.com', - 'taxId' => '123.456.789-01', - ]), - 'metadata' => PixMetadataRequest::make([ - 'externalId' => 'customer-id-123', - ]), - ]); + $this->requestDto = CreatePixQrCodeRequest::builder() + ->amount(100) + ->expiresIn(10) + ->description('abacate description') + ->customer( + PixCustomerRequest::builder() + ->name('customer name') + ->cellphone('11982516627') + ->email('joe@doe.com') + ->taxId('123.456.789-01') + ->build() + ) + ->metadata( + PixMetadataRequest::builder() + ->externalId('customer-id-123') + ->build() + ) + ->build(); }); it('should be able to create a Qr for pix', function (): void { @@ -56,9 +60,7 @@ $resource = new PixResource(client: $client); - $response = $resource->createQrCode( - $this->requestDto, - ); + $response = $resource->createQrCode($this->requestDto); expect($response)->toBeInstanceOf(CreatePixQrCodeResponse::class) ->and($response->data->id)->toBe('pix_char_123456') @@ -74,7 +76,6 @@ }); it('should throw exception when anything goes wrong', function () { - $handler = new MockHandler([ new ClientException( 'Unauthorized', @@ -86,6 +87,6 @@ $client = new Client(['handler' => $handler]); $resource = new PixResource(client: $client); - expect(fn () => $resource->createQrCode($this->requestDto)) + expect(fn() => $resource->createQrCode($this->requestDto)) ->toThrow(AbacatePayException::class, 'Token de autenticação inválido ou ausente.'); }); diff --git a/tests/Unit/Pix/PixBuilder.php b/tests/Unit/Pix/PixBuilder.php index a67b988..4f7c780 100644 --- a/tests/Unit/Pix/PixBuilder.php +++ b/tests/Unit/Pix/PixBuilder.php @@ -58,6 +58,24 @@ ]); }); +it('should allow creating a QR code with only required fields', function (): void { + $request = CreatePixQrCodeRequest::builder() + ->amount(250) + ->build(); + + expect($request) + ->toBeInstanceOf(CreatePixQrCodeRequest::class) + ->and($request->amount)->toBe(250) + ->and($request->expiresIn)->toBeNull() + ->and($request->description)->toBeNull() + ->and($request->customer)->toBeNull() + ->and($request->metadata)->toBeNull(); + + $serialized = $request->toArray(); + + expect($serialized)->toBe(['amount' => 250]); +}); + it('throws AbacatePayException when missing required fields', function () { CreatePixQrCodeRequest::builder()->build(); })->throws(AbacatePayException::class, 'Missing required fields'); From aaf11511c97f7b6841572cfd1f2659053b0de09a Mon Sep 17 00:00:00 2001 From: Clintonrocha98 Date: Fri, 31 Oct 2025 07:28:12 -0300 Subject: [PATCH 5/7] refactor: update BillingFeatureTest and add CreateBillingRequestBuilderTest for improved request validation --- tests/Feature/Billing/BillingFeatureTest.php | 8 ++++---- tests/Feature/Pix/PixResourceTest.php | 2 +- ...xBuilder.php => CreatePixQrCodeRequestBuilderTest.php} | 0 3 files changed, 5 insertions(+), 5 deletions(-) rename tests/Unit/Pix/{PixBuilder.php => CreatePixQrCodeRequestBuilderTest.php} (100%) diff --git a/tests/Feature/Billing/BillingFeatureTest.php b/tests/Feature/Billing/BillingFeatureTest.php index 7cb08d2..bad8924 100644 --- a/tests/Feature/Billing/BillingFeatureTest.php +++ b/tests/Feature/Billing/BillingFeatureTest.php @@ -111,10 +111,10 @@ products: [], return_url: 'https://example.com/return', completion_url: 'https://example.com/complete', - customerId: null, - customer: null, allow_coupons: false, coupons: [], + customerId: null, + customer: null, externalId: null ); @@ -140,10 +140,10 @@ products: [], return_url: 'https://example.com/return', completion_url: 'https://example.com/complete', - customerId: null, - customer: null, allow_coupons: false, coupons: [], + customerId: null, + customer: null, externalId: null ); diff --git a/tests/Feature/Pix/PixResourceTest.php b/tests/Feature/Pix/PixResourceTest.php index c81b721..f3b5db0 100644 --- a/tests/Feature/Pix/PixResourceTest.php +++ b/tests/Feature/Pix/PixResourceTest.php @@ -87,6 +87,6 @@ $client = new Client(['handler' => $handler]); $resource = new PixResource(client: $client); - expect(fn() => $resource->createQrCode($this->requestDto)) + expect(fn () => $resource->createQrCode($this->requestDto)) ->toThrow(AbacatePayException::class, 'Token de autenticação inválido ou ausente.'); }); diff --git a/tests/Unit/Pix/PixBuilder.php b/tests/Unit/Pix/CreatePixQrCodeRequestBuilderTest.php similarity index 100% rename from tests/Unit/Pix/PixBuilder.php rename to tests/Unit/Pix/CreatePixQrCodeRequestBuilderTest.php From 85c1b53ee104222f7fe9a882322e65bcaa755ce5 Mon Sep 17 00:00:00 2001 From: Clintonrocha98 Date: Fri, 31 Oct 2025 07:28:23 -0300 Subject: [PATCH 6/7] test: add CreateBillingRequestBuilderTest for request validation and error handling --- .../CreateBillingRequestBuilderTest.php | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 tests/Unit/Billing/CreateBillingRequestBuilderTest.php diff --git a/tests/Unit/Billing/CreateBillingRequestBuilderTest.php b/tests/Unit/Billing/CreateBillingRequestBuilderTest.php new file mode 100644 index 0000000..65e487a --- /dev/null +++ b/tests/Unit/Billing/CreateBillingRequestBuilderTest.php @@ -0,0 +1,102 @@ +product = ProductRequest::builder() + ->externalId('prod-123') + ->name('Curso PHP') + ->description('Curso completo de PHP moderno') + ->quantity(1) + ->price(15000) + ->build(); + + $this->customer = CustomerRequest::builder() + ->id('cust_abc123') + ->name('João Silva') + ->cellphone('+5511999999999') + ->email('joao@email.com') + ->taxId('12345678900') + ->build(); +}); + +it('should build a valid CreateBillingRequest', function () { + $request = CreateBillingRequest::oneTime() + ->pix() + ->returnUrl('https://retorno.exemplo.com') + ->completionUrl('https://finalizacao.exemplo.com') + ->addProduct($this->product) + ->forCustomer($this->customer) + ->build(); + + expect($request) + ->toBeInstanceOf(CreateBillingRequest::class) + ->and($request->frequency)->toBe(BillingFrequencyEnum::OneTime) + ->and($request->methods)->toBe([BillingMethodEnum::Pix]) + ->and($request->products)->toHaveCount(1) + ->and($request->customer->id)->toBe('cust_abc123') + ->and($request->allow_coupons)->toBeFalse() + ->and($request->coupons)->toBe([]); +}); + +it('should throw AbacatePayException when required fields are missing', function () { + expect(fn() => CreateBillingRequest::builder()->build()) + ->toThrow(AbacatePayException::class, 'Missing required fields'); +}); + +it('should throw AbacatePayException when both customer and customerId are set', function () { + $builder = CreateBillingRequest::oneTime() + ->pix() + ->returnUrl('https://retorno.exemplo.com') + ->completionUrl('https://finalizacao.exemplo.com') + ->addProduct($this->product) + ->forCustomer($this->customer) + ->forCustomerId('cust_conflict'); + + expect(fn() => $builder->build()) + ->toThrow(InvalidArgumentException::class); +}); + +it('should allow optional fields to be omitted', function () { + $request = CreateBillingRequest::oneTime() + ->pix() + ->returnUrl('https://retorno.exemplo.com') + ->completionUrl('https://finalizacao.exemplo.com') + ->addProduct($this->product) + ->build(); + + expect($request) + ->toBeInstanceOf(CreateBillingRequest::class) + ->and($request->externalId)->toBeNull() + ->and($request->customerId)->toBeNull() + ->and($request->customer)->toBeNull() + ->and($request->coupons)->toBe([]) + ->and($request->allow_coupons)->toBeFalse(); +}); + +it('should accept multiple methods and products', function () { + $product2 = ProductRequest::builder() + ->externalId('prod-456') + ->name('Curso Laravel') + ->description('Aprenda Laravel com exemplos práticos') + ->quantity(1) + ->price(18000) + ->build(); + + $request = CreateBillingRequest::multipleTimes() + ->methods(BillingMethodEnum::Card, BillingMethodEnum::Pix) + ->returnUrl('https://retorno.exemplo.com') + ->completionUrl('https://finalizacao.exemplo.com') + ->products($this->product, $product2) + ->build(); + + expect($request->methods)->toBe([BillingMethodEnum::Card, BillingMethodEnum::Pix]) + ->and($request->products)->toHaveCount(2); +}); From 74c01c636d96805f10341fe19aa5af30a01b2541 Mon Sep 17 00:00:00 2001 From: Clintonrocha98 Date: Fri, 31 Oct 2025 07:31:29 -0300 Subject: [PATCH 7/7] test: add CreateBillingRequestBuilderTest for request validation and error handling --- tests/Unit/Billing/CreateBillingRequestBuilderTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Unit/Billing/CreateBillingRequestBuilderTest.php b/tests/Unit/Billing/CreateBillingRequestBuilderTest.php index 65e487a..8133b33 100644 --- a/tests/Unit/Billing/CreateBillingRequestBuilderTest.php +++ b/tests/Unit/Billing/CreateBillingRequestBuilderTest.php @@ -47,7 +47,7 @@ }); it('should throw AbacatePayException when required fields are missing', function () { - expect(fn() => CreateBillingRequest::builder()->build()) + expect(fn () => CreateBillingRequest::builder()->build()) ->toThrow(AbacatePayException::class, 'Missing required fields'); }); @@ -60,7 +60,7 @@ ->forCustomer($this->customer) ->forCustomerId('cust_conflict'); - expect(fn() => $builder->build()) + expect(fn () => $builder->build()) ->toThrow(InvalidArgumentException::class); });