diff --git a/README.es.md b/README.es.md index 0a89ee8..66f0b11 100644 --- a/README.es.md +++ b/README.es.md @@ -175,18 +175,23 @@ En respuestas no-2xx, el SDK lanza `Facturapi\Exceptions\FacturapiException`. La excepción incluye: - `getMessage()`: mensaje del API cuando está disponible. - `getStatusCode()`: código HTTP. +- `getErrorCode()`: código raíz documentado de V2 cuando está disponible. - `getErrorData()`: payload JSON decodificado del error (shape completo del API). - `getRawBody()`: cuerpo crudo de la respuesta. ```php use Facturapi\Exceptions\FacturapiException; +use Facturapi\RequestErrorCode; try { $facturapi->Invoices->create($payload); } catch (FacturapiException $e) { $status = $e->getStatusCode(); $error = $e->getErrorData(); // Shape completo del error del API cuando el body es JSON válido. - $firstDetail = $error['details'][0] ?? null; // p.ej. ['path' => 'items.0.quantity', 'message' => '...', 'code' => '...'] + $firstDetail = $error['errors'][0] ?? null; // p.ej. ['path' => 'items.0.quantity', 'message' => '...', 'code' => '...'] + $retryAfter = $e->getErrorCode() === RequestErrorCode::RATE_LIMIT_EXCEEDED + ? ($e->getResponseHeaders()['retry-after'] ?? null) + : null; } ``` diff --git a/README.md b/README.md index c5ffd4f..7aec1aa 100644 --- a/README.md +++ b/README.md @@ -175,18 +175,23 @@ On non-2xx responses, the SDK throws `Facturapi\Exceptions\FacturapiException`. The exception includes: - `getMessage()`: API error message when present. - `getStatusCode()`: HTTP status code. +- `getErrorCode()`: documented V2 root error code when present. - `getErrorData()`: decoded JSON error payload (full API shape). - `getRawBody()`: raw response body string. ```php use Facturapi\Exceptions\FacturapiException; +use Facturapi\RequestErrorCode; try { $facturapi->Invoices->create($payload); } catch (FacturapiException $e) { $status = $e->getStatusCode(); $error = $e->getErrorData(); // Full API error shape when body is valid JSON. - $firstDetail = $error['details'][0] ?? null; // e.g. ['path' => 'items.0.quantity', 'message' => '...', 'code' => '...'] + $firstDetail = $error['errors'][0] ?? null; // e.g. ['path' => 'items.0.quantity', 'message' => '...', 'code' => '...'] + $retryAfter = $e->getErrorCode() === RequestErrorCode::RATE_LIMIT_EXCEEDED + ? ($e->getResponseHeaders()['retry-after'] ?? null) + : null; } ``` diff --git a/VERSION.md b/VERSION.md index ff3a77f..5ffcdcb 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1,4 +1,6 @@ -4.5.0 +4.6.0 ## Added +- Add grouped constants for documented API root error codes and Facturapi-owned validation detail codes. +- Fix `FacturapiException::getErrorCode()` to return only documented string API root codes. - Expose structured API error metadata on `FacturapiException`, including `code`, `path`, `location`, `errors`, `logId`, and response headers. diff --git a/composer.json b/composer.json index a34b01e..123c797 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,10 @@ "autoload": { "psr-4": { "Facturapi\\": "src/" - } + }, + "files": [ + "src/ErrorCodes.php" + ] }, "minimum-stability": "stable" } diff --git a/src/ErrorCodes.php b/src/ErrorCodes.php new file mode 100644 index 0000000..9c8eaa1 --- /dev/null +++ b/src/ErrorCodes.php @@ -0,0 +1,279 @@ +rawBody; } - public function getErrorCode(): mixed + public function getErrorCode(): ?string { - return is_array($this->errorData) ? ($this->errorData['code'] ?? null) : null; + return is_array($this->errorData) && is_string($this->errorData['code'] ?? null) + ? $this->errorData['code'] + : null; } public function getErrorPath(): ?string diff --git a/src/Facturapi.php b/src/Facturapi.php index b235f03..da26dfa 100644 --- a/src/Facturapi.php +++ b/src/Facturapi.php @@ -6,6 +6,7 @@ // Composer/PSR-4 users can continue relying on autoloading. require_once __DIR__ . '/Http/BaseClient.php'; require_once __DIR__ . '/Exceptions/FacturapiException.php'; +require_once __DIR__ . '/ErrorCodes.php'; require_once __DIR__ . '/Exceptions/Facturapi_Exception.php'; require_once __DIR__ . '/InvoiceRelation.php'; require_once __DIR__ . '/InvoiceType.php'; diff --git a/tests/Http/ErrorHandlingTest.php b/tests/Http/ErrorHandlingTest.php index d22e7d2..2efaee8 100644 --- a/tests/Http/ErrorHandlingTest.php +++ b/tests/Http/ErrorHandlingTest.php @@ -5,6 +5,7 @@ namespace Facturapi\Tests\Http; use Facturapi\Exceptions\FacturapiException; +use Facturapi\RequestErrorCode; use Facturapi\Resources\Invoices; use Facturapi\Tests\Support\FakeHttpClient; use GuzzleHttp\Psr7\Response; @@ -16,16 +17,9 @@ public function testApiErrorShapeIsFullyAvailableOnException(): void { $errorBody = [ 'message' => 'Request validation failed', - 'code' => 'validation_error', + 'code' => 'invalid_request', 'path' => 'customer.tax_id', 'location' => 'body', - 'details' => [ - [ - 'path' => 'customer.tax_id', - 'message' => 'customer.tax_id must be a valid RFC', - 'code' => 'invalid_rfc', - ], - ], 'errors' => [ [ 'path' => 'customer.tax_id', @@ -56,16 +50,14 @@ public function testApiErrorShapeIsFullyAvailableOnException(): void self::assertSame($errorBody, $exception->getResponseData()); self::assertSame(json_encode($errorBody), $exception->getRawBody()); - self::assertSame('validation_error', $exception->getErrorData()['code']); - self::assertSame('validation_error', $exception->getErrorCode()); + self::assertSame(RequestErrorCode::INVALID_REQUEST, $exception->getErrorData()['code']); + self::assertSame(RequestErrorCode::INVALID_REQUEST, $exception->getErrorCode()); self::assertSame('customer.tax_id', $exception->getErrorPath()); self::assertSame('body', $exception->getErrorLocation()); self::assertSame($errorBody['errors'], $exception->getErrors()); self::assertSame('log_123', $exception->getLogId()); self::assertSame('3', $exception->getResponseHeaders()['retry-after']); self::assertSame('log_123', $exception->getResponseHeaders()['x-facturapi-log-id']); - self::assertSame('customer.tax_id', $exception->getErrorData()['details'][0]['path']); - self::assertSame('invalid_rfc', $exception->getErrorData()['details'][0]['code']); } } @@ -89,4 +81,11 @@ public function testNonJsonErrorsStillExposeRawBody(): void self::assertSame($rawBody, $exception->getRawBody()); } } + + public function testNumericRootErrorCodeIsIgnored(): void + { + $exception = new FacturapiException(errorData: ['code' => 400]); + + self::assertNull($exception->getErrorCode()); + } }