diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c6ff171 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [4.8.0] 2026-09-04 + +### Added + +- Add `invoices.paymentSummary` to get the related-document object needed to build a payment complement (complemento de pago): installment number, previous balance, and taxes prorated to the paid amount. + +### Fixed + +- Return API error codes as strings and only normalize numeric API error codes. diff --git a/VERSION.md b/VERSION.md index 969152c..f37088d 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1,3 +1,8 @@ +4.7.1 + +## Fixed +- Return numeric API error codes as strings. + 4.7.0 ## Added diff --git a/src/Exceptions/FacturapiException.php b/src/Exceptions/FacturapiException.php index 958dadd..ba47d15 100644 --- a/src/Exceptions/FacturapiException.php +++ b/src/Exceptions/FacturapiException.php @@ -48,9 +48,22 @@ public function getRawBody(): ?string return $this->rawBody; } - public function getErrorCode(): mixed + public function getErrorCode(): ?string { - return is_array($this->errorData) ? ($this->errorData['code'] ?? null) : null; + if (!is_array($this->errorData) || !array_key_exists('code', $this->errorData)) { + return null; + } + + $code = $this->errorData['code']; + if ($code === null) { + return null; + } + + if (is_string($code)) { + return $code; + } + + return is_int($code) || is_float($code) ? (string) $code : null; } public function getErrorPath(): ?string diff --git a/src/Resources/Invoices.php b/src/Resources/Invoices.php index d2b6b8e..a95eef0 100644 --- a/src/Resources/Invoices.php +++ b/src/Resources/Invoices.php @@ -39,6 +39,25 @@ public function retrieve( $id ): mixed { } } + /** + * Gets the information needed to add this invoice as a related document in a + * payment complement (complemento de pago): the installment number according + * to the payment history, the previous balance, and the invoice tax breakdown + * prorated to the amount being paid. + * + * @param string $id Invoice ID. + * @param float $amount Amount being paid, expressed in the invoice currency. Cannot exceed the outstanding balance. + * @return mixed JSON-decoded related document summary. + * @throws FacturapiException + */ + public function paymentSummary( $id, $amount ): mixed { + try { + return json_decode( $this->executeGetRequest( $this->getRequestUrl( $id . '/payment-summary', array( 'amount' => $amount ) ) ) ); + } catch ( FacturapiException $e ) { + throw $e; + } + } + /** * Create an Invoice in your organization * diff --git a/tests/Http/ErrorHandlingTest.php b/tests/Http/ErrorHandlingTest.php index d22e7d2..61df2e0 100644 --- a/tests/Http/ErrorHandlingTest.php +++ b/tests/Http/ErrorHandlingTest.php @@ -89,4 +89,20 @@ public function testNonJsonErrorsStillExposeRawBody(): void self::assertSame($rawBody, $exception->getRawBody()); } } + + public function testNumericApiErrorCodesAreConvertedToStrings(): void + { + $httpClient = new FakeHttpClient( + new Response(400, ['Content-Type' => 'application/json'], '{"code": 400}') + ); + + $invoices = new Invoices('sk_test_abc123', ['httpClient' => $httpClient]); + + try { + $invoices->create(['customer' => []]); + self::fail('Expected FacturapiException to be thrown.'); + } catch (FacturapiException $exception) { + self::assertSame('400', $exception->getErrorCode()); + } + } } diff --git a/tests/Resources/InvoicesTest.php b/tests/Resources/InvoicesTest.php index 817ff0e..9274bcd 100644 --- a/tests/Resources/InvoicesTest.php +++ b/tests/Resources/InvoicesTest.php @@ -141,4 +141,32 @@ public function testDownloadZipRequestReturnsBinaryContentsFromExpectedPath(): v self::assertSame('GET', $request->getMethod()); self::assertSame('https://www.facturapi.io/v2/invoices/zip-requests/zip_123/zip', (string) $request->getUri()); } + + public function testPaymentSummaryUsesExpectedPathAndQueryParameter(): void + { + $summary = [ + 'uuid' => '6CF6CE33-1BD2-4F88-A443-33013C069169', + 'installment' => 1, + 'last_balance' => 100.0, + 'total' => 100.0, + 'currency' => 'MXN', + 'amount' => 58.0, + 'taxes' => [ + ['base' => 50.0, 'rate' => 0.16, 'type' => 'IVA', 'factor' => 'Tasa', 'withholding' => false], + ], + ]; + $httpClient = new FakeHttpClient(new Response(200, [], json_encode($summary))); + $invoices = new Invoices('sk_test_abc123', ['httpClient' => $httpClient]); + + $result = $invoices->paymentSummary('inv_123', 58.0); + + self::assertSame('6CF6CE33-1BD2-4F88-A443-33013C069169', $result->uuid); + self::assertSame(1, $result->installment); + self::assertSame(50.0, (float) $result->taxes[0]->base); + + $request = $httpClient->requests()[0]; + self::assertSame('GET', $request->getMethod()); + self::assertSame('https://www.facturapi.io/v2/invoices/inv_123/payment-summary?amount=58', (string) $request->getUri()); + self::assertSame('Basic ' . base64_encode('sk_test_abc123:'), $request->getHeaderLine('Authorization')); + } }