From d91574b25b195d54c5d7e6f6e326ad54a9814804 Mon Sep 17 00:00:00 2001 From: javorosas Date: Sun, 29 Mar 2026 23:50:54 +0200 Subject: [PATCH 01/16] refactor: modernize v4 sdk internals and error propagation --- composer.json | 13 +- example.php | 5 +- src/Exceptions/FacturapiException.php | 7 + src/Exceptions/Facturapi_Exception.php | 10 +- src/Facturapi.php | 55 ++- src/Http/BaseClient.php | 550 ++++++++++++--------- src/Resources/CartaPorteCatalogs.php | 147 +++--- src/Resources/Catalogs.php | 42 +- src/Resources/ComercioExteriorCatalogs.php | 21 +- src/Resources/Customers.php | 115 ++--- src/Resources/Invoices.php | 358 +++++++++----- src/Resources/Organizations.php | 468 +++++++++--------- src/Resources/Products.php | 93 ++-- src/Resources/Receipts.php | 163 +++--- src/Resources/Retentions.php | 182 ++++--- src/Resources/Tools.php | 39 +- src/Resources/Webhooks.php | 107 ++-- 17 files changed, 1309 insertions(+), 1066 deletions(-) create mode 100644 src/Exceptions/FacturapiException.php diff --git a/composer.json b/composer.json index f570cbb..6c0bdac 100644 --- a/composer.json +++ b/composer.json @@ -20,16 +20,17 @@ } ], "require": { - "php": ">=5.5.0" + "php": ">=8.2", + "guzzlehttp/guzzle": "^7.9", + "psr/http-client": "^1.0" }, "require-dev": { - "phpspec/phpspec": "~2.0", - "phpunit/phpunit": "~4.0" + "phpunit/phpunit": "^11.5" }, "autoload": { - "classmap": [ - "/src" - ] + "psr-4": { + "Facturapi\\": "src/" + } }, "minimum-stability": "stable" } diff --git a/example.php b/example.php index fbcc58e..847356c 100644 --- a/example.php +++ b/example.php @@ -2,10 +2,11 @@ use Facturapi\Facturapi; -require_once 'src/Facturapi.php'; +require_once __DIR__ . '/vendor/autoload.php'; -$facturapi = new Facturapi( FACTURAPI_KEY ); +$apiKey = getenv('FACTURAPI_KEY') ?: 'YOUR_API_KEY'; +$facturapi = new Facturapi( $apiKey ); var_dump( $facturapi->Invoices->retrieve( "5a3f54cff508333611ad6b40" ) ); diff --git a/src/Exceptions/FacturapiException.php b/src/Exceptions/FacturapiException.php new file mode 100644 index 0000000..43deadb --- /dev/null +++ b/src/Exceptions/FacturapiException.php @@ -0,0 +1,7 @@ +Customers = new Customers($api_key, $api_version); - $this->Organizations = new Organizations($api_key, $api_version); - $this->Products = new Products($api_key, $api_version); - $this->Invoices = new Invoices($api_key, $api_version); - $this->Receipts = new Receipts($api_key, $api_version); - $this->Catalogs = new Catalogs($api_key, $api_version); - $this->CartaPorteCatalogs = new CartaPorteCatalogs($api_key, $api_version); - $this->Retentions = new Retentions($api_key, $api_version); - $this->Tools = new Tools($api_key, $api_version); - $this->Webhooks = new Webhooks($api_key, $api_version); + $this->Customers = new Customers($apiKey, $config); + $this->Organizations = new Organizations($apiKey, $config); + $this->Products = new Products($apiKey, $config); + $this->Invoices = new Invoices($apiKey, $config); + $this->Receipts = new Receipts($apiKey, $config); + $this->Catalogs = new Catalogs($apiKey, $config); + $this->CartaPorteCatalogs = new CartaPorteCatalogs($apiKey, $config); + $this->ComercioExteriorCatalogs = new ComercioExteriorCatalogs($apiKey, $config); + $this->Retentions = new Retentions($apiKey, $config); + $this->Tools = new Tools($apiKey, $config); + $this->Webhooks = new Webhooks($apiKey, $config); } } diff --git a/src/Http/BaseClient.php b/src/Http/BaseClient.php index 6d6741a..09f6c41 100644 --- a/src/Http/BaseClient.php +++ b/src/Http/BaseClient.php @@ -2,21 +2,34 @@ namespace Facturapi\Http; -use Facturapi\Exceptions\Facturapi_Exception; +use Facturapi\Exceptions\FacturapiException; +use GuzzleHttp\Client; +use GuzzleHttp\Psr7\MultipartStream; +use GuzzleHttp\Psr7\Request; +use GuzzleHttp\Psr7\Utils; +use Psr\Http\Client\ClientExceptionInterface; +use Psr\Http\Client\ClientInterface; class BaseClient { // BaseClient class to be extended by specific clients - protected $FACTURAPI_KEY; - protected $ENDPOINT; - protected $API_VERSION = 'v2'; - protected $BASE_URL = 'https://www.facturapi.io/'; + protected string $FACTURAPI_KEY; + protected string $ENDPOINT = ''; + protected string $API_VERSION = 'v2'; + protected string $BASE_URL = 'https://www.facturapi.io/'; + protected float $CONNECT_TIMEOUT = 3.0; + protected float $TIMEOUT = 360.0; + protected string $USER_AGENT = 'facturapi-php'; /** * The HTTP status of the most recent request * - * @var integer + * @var int|null */ - protected $lastStatus; + protected ?int $lastStatus = null; + /** + * @var ClientInterface + */ + protected ClientInterface $httpClient; /** * The HTTP code for a successful request */ @@ -37,20 +50,35 @@ class BaseClient /** * Constructor. * - * @param $FACTURAPI_KEY : String value of Facturapi API Key for requests + * @param string $apiKey Facturapi API key. + * @param array{apiVersion?:string,timeout?:int|float,httpClient?:ClientInterface}|null $config + * Optional SDK config. Supported keys: apiVersion, timeout, httpClient. + * @throws FacturapiException */ - public function __construct($FACTURAPI_KEY, $API_VERSION = 'v2') + public function __construct(string $apiKey, ?array $config = null) { - $this->FACTURAPI_KEY = base64_encode($FACTURAPI_KEY . ":"); - $this->API_VERSION = $API_VERSION; + $this->FACTURAPI_KEY = base64_encode($apiKey . ":"); + + $normalized = $this->normalizeConfig($config); + $this->API_VERSION = $normalized['apiVersion']; + $httpClient = $normalized['httpClient']; + $this->applyHttpConfig($normalized['httpConfig']); + + $this->httpClient = $httpClient ?: new Client(array( + 'timeout' => $this->TIMEOUT, + 'connect_timeout' => $this->CONNECT_TIMEOUT, + 'verify' => true, + 'http_errors' => false, + 'allow_redirects' => true, + )); } /** - * Gets the status code from the most recent curl request + * Gets the status code from the most recent HTTP request. * - * @return integer + * @return int */ - public function getLastStatus() + public function getLastStatus(): int { return (int) $this->lastStatus; } @@ -61,15 +89,15 @@ public function getLastStatus() * base path for the API (e.g.: the customers api sets the value to * 'customers') * - * @throws Facturapi_Exception + * @throws FacturapiException */ - protected function get_endpoint() + protected function getEndpoint(): string { if (empty($this->ENDPOINT)) { - throw new Facturapi_Exception('ENDPOINT must be defined'); - } else { - return $this->ENDPOINT; + throw new FacturapiException('ENDPOINT must be defined'); } + + return $this->ENDPOINT; } /** @@ -78,301 +106,375 @@ protected function get_endpoint() * version that the client is developed for (e.g.: the customers v1 * client sets the value to 'v1') * - * @throws Facturapi_Exception + * @throws FacturapiException */ - protected function get_api_version() + protected function getApiVersion(): string { if (empty($this->API_VERSION)) { - throw new Facturapi_Exception('API_VERSION must be defined'); - } else { - return $this->API_VERSION; + throw new FacturapiException('API_VERSION must be defined'); } + + return $this->API_VERSION; } /** * Creates the url to be used for the api request * - * @param params : Array containing query parameters and values - * - * @returns String + * @param array|string|null $params Path segment or query parameters. + * @param array|null $query Query parameters for nested endpoints. + * @return string */ - protected function get_request_url($params = null, $query = null) + protected function getRequestUrl($params = null, $query = null): string { $param_string = $params == null ? "" : ( is_string($params) ? ($query == null ? "/" . $params - : "/" . $params . "/" . $this->array_to_params($query) - ) : $this->array_to_params($params) + : "/" . $params . $this->arrayToParams($query) + ) : $this->arrayToParams($params) ); - return $this->BASE_URL . $this->get_api_version() . "/" . $this->get_endpoint() . $param_string; + return $this->BASE_URL . $this->getApiVersion() . "/" . $this->getEndpoint() . $param_string; } /** * Executes HTTP GET request * - * @param URL : String value for the URL to GET - * - * @return String Body of request result + * @param string $url URL to call. + * @return string Response body. * - * @throws Facturapi_Exception + * @throws FacturapiException */ - protected function execute_get_request($url) + protected function executeGetRequest($url): string { - $headers[] = 'Authorization: Basic ' . $this->FACTURAPI_KEY; - - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - curl_setopt($ch, CURLOPT_ENCODING, "gzip"); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); - - $output = curl_exec($ch); - $errno = curl_errno($ch); - $error = curl_error($ch); - $responseCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - $this->setLastStatusFromCurl($ch); - curl_close($ch); - if ($errno > 0) { - throw new Facturapi_Exception('cURL error: ' . $error); - } elseif ($responseCode < 200 || $responseCode > 299) { - throw new Facturapi_Exception($output); - } else { - return $output; - } + return $this->executeRequest('GET', $url, array('Accept-Encoding' => 'gzip')); } /** * Executes HTTP POST request * - * @param URL : String value for the URL to POST to - * @param fields : Array containing names and values for fields to post - * - * @return Body of request result + * @param string $url URL to call. + * @param mixed $body Request body. + * @param bool $formenc Whether to send as form-urlencoded. + * @return string Response body. * - * @throws Facturapi_Exception + * @throws FacturapiException */ - protected function execute_post_request($url, $body, $formenc = false) + protected function executePostRequest($url, $body, $formenc = false): string { - $headers[] = 'Authorization: Basic ' . $this->FACTURAPI_KEY; + $headers = array(); + $payload = null; + if ($formenc) { - $headers[] = 'Content-Type: application/x-www-form-urlencoded'; - } - // initialize cURL and send POST data - $ch = curl_init(); - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_POSTFIELDS, $body); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - - - $output = curl_exec($ch); - $errno = curl_errno($ch); - $error = curl_error($ch); - $responseCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - $this->setLastStatusFromCurl($ch); - curl_close($ch); - if ($errno > 0) { - throw new Facturapi_Exception('cURL error: ' . $error); - } elseif ($responseCode < 200 || $responseCode > 299) { - throw new Facturapi_Exception($output); - } else { - return $output; + $headers['Content-Type'] = 'application/x-www-form-urlencoded'; + $payload = is_array($body) ? http_build_query($body) : (string) $body; + } elseif (is_array($body)) { + $payload = http_build_query($body); + } elseif ($body !== null) { + $payload = (string) $body; } + + return $this->executeRequest('POST', $url, $headers, $payload); } /** * Executes HTTP POST request with JSON as the POST body * - * @param URL String value for the URL to POST to - * @param fields array containing names and values for fields to post - * - * @return String Body of request result + * @param string $url URL to call. + * @param array|null $body Request body. + * @return string Response body. * - * @throws Facturapi_Exception + * @throws FacturapiException */ - protected function execute_JSON_post_request($url, $body = null) + protected function executeJsonPostRequest($url, $body = null): string { - $headers[] = 'Authorization: Basic ' . $this->FACTURAPI_KEY; - $headers[] = 'Content-Type: application/json'; - - // initialize cURL and send POST data - $ch = curl_init(); - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_POSTFIELDS, $body ? json_encode($body) : null); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - - $output = curl_exec($ch); - $errno = curl_errno($ch); - $error = curl_error($ch); - //Get response code, only numbers in range from 200 to 299 are valid - $responseCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - $this->setLastStatusFromCurl($ch); - curl_close($ch); - if ($errno > 0) { - throw new Facturapi_Exception('cURL error: ' . $error); - } elseif ($responseCode < 200 || $responseCode > 299) { - throw new Facturapi_Exception($output); - } else { - return $output; + $payload = null; + if ($body !== null) { + $payload = json_encode($body); + if ($payload === false) { + throw new FacturapiException('Unable to encode JSON payload: ' . json_last_error_msg()); + } } + + return $this->executeRequest('POST', $url, array('Content-Type' => 'application/json'), $payload); } /** * Executes HTTP PUT request * - * @param URL String value for the URL to PUT to - * @param array $body + * @param string $url URL to call. + * @param array|null $body Request body. * - * @return String Body of request result + * @return string Response body. * - * @throws Facturapi_Exception + * @throws FacturapiException */ - protected function execute_JSON_put_request($url, $body) + protected function executeJsonPutRequest($url, $body): string { - $headers[] = 'Authorization: Basic ' . $this->FACTURAPI_KEY; - $headers[] = 'Content-Type: application/json'; - - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT"); - curl_setopt($ch, CURLOPT_POSTFIELDS, $body ? json_encode($body) : null); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); - - $result = curl_exec($ch); - $errno = curl_errno($ch); - $error = curl_error($ch); - $responseCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - $this->setLastStatusFromCurl($ch); - curl_close($ch); - if ($errno > 0) { - throw new Facturapi_Exception('cURL error: ' . $error); - } elseif ($responseCode < 200 || $responseCode > 299) { - throw new Facturapi_Exception($result); - } else { - return $result; + $payload = null; + if ($body !== null) { + $payload = json_encode($body); + if ($payload === false) { + throw new FacturapiException('Unable to encode JSON payload: ' . json_last_error_msg()); + } } + + return $this->executeRequest('PUT', $url, array('Content-Type' => 'application/json'), $payload); } /** * Executes HTTP PUT request * - * @param URL String value for the URL to PUT to - * @param array $body + * @param string $url URL to call. + * @param array|string $body Multipart payload definition. * - * @return String Body of request result + * @return string Response body. * - * @throws Facturapi_Exception + * @throws FacturapiException */ - protected function execute_data_put_request($url, $body) + protected function executeDataPutRequest($url, $body): string { - $headers[] = 'Authorization: Basic ' . $this->FACTURAPI_KEY; - $headers[] = 'Content-Type: multipart/form-data'; - - $data = is_array($body) ? array( - 'cer' => new \CURLFile($body['cerFile']), - 'key' => new \CURLFile($body['keyFile']), - 'password' => $body['password'] - ) : array( - 'file' => new \CURLFile($body) - ); + if (is_array($body)) { + if (!isset($body['cerFile'], $body['keyFile'], $body['password'])) { + throw new FacturapiException('Invalid certificate payload. Expected cerFile, keyFile and password.'); + } - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT"); - curl_setopt($ch, CURLOPT_POSTFIELDS, $data); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); - - $result = curl_exec($ch); - $errno = curl_errno($ch); - $error = curl_error($ch); - $this->setLastStatusFromCurl($ch); - curl_close($ch); - if ($errno > 0) { - throw new Facturapi_Exception('cURL error: ' . $error); + $multipart = new MultipartStream(array( + array( + 'name' => 'cer', + 'contents' => Utils::streamFor($this->openFileStream($body['cerFile'])), + 'filename' => basename($body['cerFile']) + ), + array( + 'name' => 'key', + 'contents' => Utils::streamFor($this->openFileStream($body['keyFile'])), + 'filename' => basename($body['keyFile']) + ), + array( + 'name' => 'password', + 'contents' => (string) $body['password'] + ), + )); } else { - return $result; + $multipart = new MultipartStream(array( + array( + 'name' => 'file', + 'contents' => Utils::streamFor($this->openFileStream($body)), + 'filename' => basename($body) + ), + )); } + + $headers = array( + 'Content-Type' => 'multipart/form-data; boundary=' . $multipart->getBoundary(), + ); + + return $this->executeRequest('PUT', $url, $headers, $multipart, false); } /** * Executes HTTP DELETE request * - * @param URL String value for the URL to DELETE to - * @param String $body + * @param string $url URL to call. + * @param string|null $body Request body. * - * @return String Body of request result + * @return string Response body. * - * @throws Facturapi_Exception + * @throws FacturapiException */ - protected function execute_delete_request($url, $body) + protected function executeDeleteRequest($url, $body): string { - $headers[] = 'Authorization: Basic ' . $this->FACTURAPI_KEY; - $headers[] = 'Content-Type: application/json'; - $headers[] = 'Content-Length: ' . strlen($body); - - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "DELETE"); - curl_setopt($ch, CURLOPT_POSTFIELDS, $body); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); - - $result = curl_exec($ch); - $errno = curl_errno($ch); - $error = curl_error($ch); - $this->setLastStatusFromCurl($ch); - curl_close($ch); - if ($errno > 0) { - throw new Facturapi_Exception('cURL error: ' . $error); - } else { - return $result; - } + $payload = $body === null ? null : (string) $body; + return $this->executeRequest('DELETE', $url, array('Content-Type' => 'application/json'), $payload, false); } /** * Converts an array into url friendly list of parameters * - * @param array params Multidimensional array of parameters (name=>value) - * - * @return String of url friendly parameters (&name=value&foo=bar) + * @param array|null $params Parameters (name => value). + * @return string URL query string. */ - protected function array_to_params($params) + protected function arrayToParams($params): string { - $param_string = '?'; - if ($params != null) { - foreach ($params as $parameter => $value) { - if (is_array($value)) { - foreach ($value as $key => $sub_param) { - $param_string = $param_string . '&' . $parameter . '[' . $key . ']' . '=' . urlencode($sub_param); - } - } else { - $param = gettype($value) == 'boolean' ? json_encode($value) : urlencode($value); - $param_string = $param_string . '&' . $parameter . '=' . $param; + if ($params == null) { + return ''; + } + + $parts = array(); + foreach ($params as $parameter => $value) { + if (is_array($value)) { + foreach ($value as $key => $sub_param) { + $parts[] = $parameter . '[' . $key . ']' . '=' . urlencode((string) $sub_param); } + } else { + $param = is_bool($value) ? ($value ? 'true' : 'false') : urlencode((string) $value); + $parts[] = $parameter . '=' . $param; } } - return $param_string; + return empty($parts) ? '' : '?' . implode('&', $parts); } /** - * Sets the status code from a curl request + * Executes an HTTP request and optionally validates 2xx status. + * + * @param string $method HTTP method. + * @param string $url URL to call. + * @param array $headers Request headers. + * @param mixed $body Request body. + * @param bool $validateStatus Whether to throw on non-2xx status. + * @return string Response body. + * @throws FacturapiException */ - protected function setLastStatusFromCurl($ch) + protected function executeRequest($method, $url, $headers = array(), $body = null, $validateStatus = true): string { - $info = curl_getinfo($ch); - $this->lastStatus = (isset($info['http_code'])) ? $info['http_code'] : null; + $request = new Request( + $method, + $url, + $this->buildHeaders($headers), + $body === null ? '' : $body + ); + + try { + $response = $this->httpClient->sendRequest($request); + } catch (ClientExceptionInterface $e) { + throw new FacturapiException($e->getMessage(), 0, $e); + } + + $this->lastStatus = $response->getStatusCode(); + $output = (string) $response->getBody(); + + if ($validateStatus && ($this->lastStatus < 200 || $this->lastStatus > 299)) { + throw new FacturapiException($output); + } + + return $output; + } + + /** + * Builds base headers for API requests. + * + * @param array $headers Additional request headers. + * @return array + */ + protected function buildHeaders($headers): array + { + $base_headers = array( + 'Authorization' => 'Basic ' . $this->FACTURAPI_KEY, + 'User-Agent' => $this->USER_AGENT, + ); + + foreach ($headers as $name => $value) { + $base_headers[$name] = $value; + } + + return $base_headers; + } + + /** + * Opens a file stream for multipart uploads. + * + * @param string $path File path. + * @return resource + * @throws FacturapiException + */ + protected function openFileStream($path) + { + $stream = @fopen($path, 'rb'); + if ($stream === false) { + throw new FacturapiException('Unable to open file: ' . $path); + } + + return $stream; + } + + /** + * Applies optional HTTP configuration for the internal client. + * + * @param array $httpConfig HTTP settings. + * @throws FacturapiException + */ + protected function applyHttpConfig(array $httpConfig): void + { + if (array_key_exists('timeout', $httpConfig)) { + $this->TIMEOUT = $this->validateTimeoutValue($httpConfig['timeout'], 'timeout'); + } + } + + /** + * Normalizes constructor HTTP config. + * + * @param array{apiVersion?:string,timeout?:int|float,httpClient?:ClientInterface}|null $config + * @return array{apiVersion:string,httpClient:ClientInterface|null,httpConfig:array} + * @throws FacturapiException + */ + protected function normalizeConfig(?array $config): array + { + if ($config === null) { + return array( + 'apiVersion' => 'v2', + 'httpClient' => null, + 'httpConfig' => array() + ); + } + + $allowedKeys = array('apiVersion', 'timeout', 'httpClient'); + foreach (array_keys($config) as $configKey) { + if (!in_array($configKey, $allowedKeys, true)) { + throw new FacturapiException('SDK config "' . $configKey . '" is not supported. Allowed keys: apiVersion, timeout, httpClient.'); + } + } + + $apiVersion = 'v2'; + if (array_key_exists('apiVersion', $config)) { + $version = $config['apiVersion']; + if (!is_string($version) || trim($version) === '') { + throw new FacturapiException('Config key "apiVersion" must be a non-empty string.'); + } + $apiVersion = $version; + } + + $httpClient = $config['httpClient'] ?? null; + + if ($httpClient !== null && !($httpClient instanceof ClientInterface)) { + throw new FacturapiException('Config key "httpClient" must implement Psr\\Http\\Client\\ClientInterface.'); + } + + if ($httpClient !== null && array_key_exists('timeout', $config)) { + throw new FacturapiException('When using "httpClient", configure timeout directly on that client.'); + } + + $httpConfig = array(); + if (array_key_exists('timeout', $config)) { + $httpConfig['timeout'] = $config['timeout']; + } + + return array( + 'apiVersion' => $apiVersion, + 'httpClient' => $httpClient, + 'httpConfig' => $httpConfig + ); + } + + /** + * Validates timeout values. + * + * @param mixed $value Timeout value in seconds. + * @param string $name Option name. + * @return float + * @throws FacturapiException + */ + protected function validateTimeoutValue(mixed $value, string $name): float + { + if (!is_numeric($value)) { + throw new FacturapiException('HTTP config "' . $name . '" must be numeric (seconds)'); + } + + $timeout = (float) $value; + if ($timeout <= 0) { + throw new FacturapiException('HTTP config "' . $name . '" must be greater than 0'); + } + + return $timeout; } } diff --git a/src/Resources/CartaPorteCatalogs.php b/src/Resources/CartaPorteCatalogs.php index c4529c0..9de54e0 100644 --- a/src/Resources/CartaPorteCatalogs.php +++ b/src/Resources/CartaPorteCatalogs.php @@ -3,172 +3,209 @@ namespace Facturapi\Resources; use Facturapi\Http\BaseClient; -use Facturapi\Exceptions\Facturapi_Exception; +use Facturapi\Exceptions\FacturapiException; class CartaPorteCatalogs extends BaseClient { - protected $ENDPOINT = 'catalogs/cartaporte/3.1'; + protected string $ENDPOINT = 'catalogs/cartaporte/3.1'; /** * Air transport codes (Carta Porte 3.1) - * @param $params Search parameters - * @return JSON search result - * @throws Facturapi_Exception + * + * @param array|null $params Search parameters. + * @return mixed JSON-decoded response. + * @throws FacturapiException */ - public function searchAirTransportCodes($params = null) + public function searchAirTransportCodes($params = null): mixed { try { return json_decode( - $this->execute_get_request( - $this->get_request_url("air-transport-codes") . $this->array_to_params($params) + $this->executeGetRequest( + $this->getRequestUrl("air-transport-codes", $params) ) ); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to search air transport codes: ' . $e->getMessage()); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** * Auto transport configurations (Carta Porte 3.1) + * + * @param array|null $params Search parameters. + * @return mixed JSON-decoded response. + * @throws FacturapiException */ - public function searchTransportConfigs($params = null) + public function searchTransportConfigs($params = null): mixed { try { return json_decode( - $this->execute_get_request( - $this->get_request_url("transport-configs") . $this->array_to_params($params) + $this->executeGetRequest( + $this->getRequestUrl("transport-configs", $params) ) ); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to search transport configurations: ' . $e->getMessage()); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** * Rights of passage (Carta Porte 3.1) + * + * @param array|null $params Search parameters. + * @return mixed JSON-decoded response. + * @throws FacturapiException */ - public function searchRightsOfPassage($params = null) + public function searchRightsOfPassage($params = null): mixed { try { return json_decode( - $this->execute_get_request( - $this->get_request_url("rights-of-passage") . $this->array_to_params($params) + $this->executeGetRequest( + $this->getRequestUrl("rights-of-passage", $params) ) ); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to search rights of passage: ' . $e->getMessage()); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** * Customs documents (Carta Porte 3.1) + * + * @param array|null $params Search parameters. + * @return mixed JSON-decoded response. + * @throws FacturapiException */ - public function searchCustomsDocuments($params = null) + public function searchCustomsDocuments($params = null): mixed { try { return json_decode( - $this->execute_get_request( - $this->get_request_url("customs-documents") . $this->array_to_params($params) + $this->executeGetRequest( + $this->getRequestUrl("customs-documents", $params) ) ); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to search customs documents: ' . $e->getMessage()); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** * Packaging types (Carta Porte 3.1) + * + * @param array|null $params Search parameters. + * @return mixed JSON-decoded response. + * @throws FacturapiException */ - public function searchPackagingTypes($params = null) + public function searchPackagingTypes($params = null): mixed { try { return json_decode( - $this->execute_get_request( - $this->get_request_url("packaging-types") . $this->array_to_params($params) + $this->executeGetRequest( + $this->getRequestUrl("packaging-types", $params) ) ); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to search packaging types: ' . $e->getMessage()); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** * Trailer types (Carta Porte 3.1) + * + * @param array|null $params Search parameters. + * @return mixed JSON-decoded response. + * @throws FacturapiException */ - public function searchTrailerTypes($params = null) + public function searchTrailerTypes($params = null): mixed { try { return json_decode( - $this->execute_get_request( - $this->get_request_url("trailer-types") . $this->array_to_params($params) + $this->executeGetRequest( + $this->getRequestUrl("trailer-types", $params) ) ); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to search trailer types: ' . $e->getMessage()); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** * Hazardous materials (Carta Porte 3.1) + * + * @param array|null $params Search parameters. + * @return mixed JSON-decoded response. + * @throws FacturapiException */ - public function searchHazardousMaterials($params = null) + public function searchHazardousMaterials($params = null): mixed { try { return json_decode( - $this->execute_get_request( - $this->get_request_url("hazardous-materials") . $this->array_to_params($params) + $this->executeGetRequest( + $this->getRequestUrl("hazardous-materials", $params) ) ); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to search hazardous materials: ' . $e->getMessage()); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** * Naval authorizations (Carta Porte 3.1) + * + * @param array|null $params Search parameters. + * @return mixed JSON-decoded response. + * @throws FacturapiException */ - public function searchNavalAuthorizations($params = null) + public function searchNavalAuthorizations($params = null): mixed { try { return json_decode( - $this->execute_get_request( - $this->get_request_url("naval-authorizations") . $this->array_to_params($params) + $this->executeGetRequest( + $this->getRequestUrl("naval-authorizations", $params) ) ); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to search naval authorizations: ' . $e->getMessage()); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** * Port stations (air/sea/land) (Carta Porte 3.1) + * + * @param array|null $params Search parameters. + * @return mixed JSON-decoded response. + * @throws FacturapiException */ - public function searchPortStations($params = null) + public function searchPortStations($params = null): mixed { try { return json_decode( - $this->execute_get_request( - $this->get_request_url("port-stations") . $this->array_to_params($params) + $this->executeGetRequest( + $this->getRequestUrl("port-stations", $params) ) ); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to search port stations: ' . $e->getMessage()); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** * Marine containers (Carta Porte 3.1) + * + * @param array|null $params Search parameters. + * @return mixed JSON-decoded response. + * @throws FacturapiException */ - public function searchMarineContainers($params = null) + public function searchMarineContainers($params = null): mixed { try { return json_decode( - $this->execute_get_request( - $this->get_request_url("marine-containers") . $this->array_to_params($params) + $this->executeGetRequest( + $this->getRequestUrl("marine-containers", $params) ) ); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to search marine containers: ' . $e->getMessage()); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } } diff --git a/src/Resources/Catalogs.php b/src/Resources/Catalogs.php index 2ebb111..434f4ed 100644 --- a/src/Resources/Catalogs.php +++ b/src/Resources/Catalogs.php @@ -3,53 +3,51 @@ namespace Facturapi\Resources; use Facturapi\Http\BaseClient; -use Facturapi\Exceptions\Facturapi_Exception; +use Facturapi\Exceptions\FacturapiException; class Catalogs extends BaseClient { - protected $ENDPOINT = 'catalogs'; + protected string $ENDPOINT = 'catalogs'; /** * Search a product key in SAT's catalog * - * @param $params Search parameters + * @param array|null $params Search parameters. + * @return mixed JSON-decoded response. * - * @return JSON objects search result - * - * @throws Facturapi_Exception - **/ - public function searchProducts($params = null) + * @throws FacturapiException + */ + public function searchProducts($params = null): mixed { try { return json_decode( - $this->execute_get_request( - $this->get_request_url("products") . $this->array_to_params($params) + $this->executeGetRequest( + $this->getRequestUrl("products", $params) ) ); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to search products: ' . $e->getMessage()); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** * Search a unit key in SAT's catalog * - * @param $params Search parameters - * - * @return JSON search result + * @param array|null $params Search parameters. + * @return mixed JSON-decoded response. * - * @throws Facturapi_Exception - **/ - public function searchUnits($params = null) + * @throws FacturapiException + */ + public function searchUnits($params = null): mixed { try { return json_decode( - $this->execute_get_request( - $this->get_request_url("units") . $this->array_to_params($params) + $this->executeGetRequest( + $this->getRequestUrl("units", $params) ) ); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to search unit keys: ' . $e->getMessage()); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } } diff --git a/src/Resources/ComercioExteriorCatalogs.php b/src/Resources/ComercioExteriorCatalogs.php index 37c524e..2adef5e 100644 --- a/src/Resources/ComercioExteriorCatalogs.php +++ b/src/Resources/ComercioExteriorCatalogs.php @@ -3,28 +3,29 @@ namespace Facturapi\Resources; use Facturapi\Http\BaseClient; -use Facturapi\Exceptions\Facturapi_Exception; +use Facturapi\Exceptions\FacturapiException; class ComercioExteriorCatalogs extends BaseClient { - protected $ENDPOINT = 'catalogs/comercioexterior/2.0'; + protected string $ENDPOINT = 'catalogs/comercioexterior/2.0'; /** - * Search tariff fractions (Fracciones Arancelarias) + * Search tariff fractions (Fracciones Arancelarias). + * * @param array|null $params Search parameters (e.g., ["q" => "0101", "page" => 0, "limit" => 10]) - * @return JSON search result - * @throws Facturapi_Exception + * @return mixed JSON-decoded response. + * @throws FacturapiException */ - public function searchTariffFractions($params = null) + public function searchTariffFractions($params = null): mixed { try { return json_decode( - $this->execute_get_request( - $this->get_request_url("tariff-fractions") . $this->array_to_params($params) + $this->executeGetRequest( + $this->getRequestUrl("tariff-fractions", $params) ) ); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to search tariff fractions: ' . $e->getMessage()); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } } diff --git a/src/Resources/Customers.php b/src/Resources/Customers.php index e8eeef2..a3a7a9f 100644 --- a/src/Resources/Customers.php +++ b/src/Resources/Customers.php @@ -3,61 +3,57 @@ namespace Facturapi\Resources; use Facturapi\Http\BaseClient; -use Facturapi\Exceptions\Facturapi_Exception; +use Facturapi\Exceptions\FacturapiException; class Customers extends BaseClient { - protected $ENDPOINT = 'customers'; + protected string $ENDPOINT = 'customers'; /** * Get all Customers * - * @param $params Search parameters + * @param array|null $params Search parameters. + * @return mixed JSON-decoded response. * - * @return JSON objects for all Customers - * - * @throws Facturapi_Exception - **/ - public function all( $params = null ) { + * @throws FacturapiException + */ + public function all( $params = null ): mixed { try { - return json_decode( $this->execute_get_request( $this->get_request_url( $params ) ) ); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to get customers: ' . $e->getMessage() ); + return json_decode( $this->executeGetRequest( $this->getRequestUrl( $params ) ) ); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** * Get a Customer by ID * - * @param string $id : Unique ID for customer - * - * @return JSON object for requested Customer + * @param string $id Customer ID. + * @return mixed JSON-decoded response. * - * @throws Facturapi_Exception - **/ - public function retrieve( $id ) { + * @throws FacturapiException + */ + public function retrieve( $id ): mixed { try { - return json_decode( $this->execute_get_request( $this->get_request_url( $id ) ) ); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to get customer: ' . $e->getMessage() ); + return json_decode( $this->executeGetRequest( $this->getRequestUrl( $id ) ) ); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** * Create a Customer in your organization * - * @param $data : array of properties and property values for new customer - * @param $params : array of optional query parameters - * - * @return Response body with JSON object - * for created Customer from HTTP POST request + * @param array $data Customer data. + * @param array|null $params Optional query parameters. + * @return mixed JSON-decoded response. * - * @throws Facturapi_Exception - **/ - public function create( $data, $params = null ) { + * @throws FacturapiException + */ + public function create( $data, $params = null ): mixed { try { - return json_decode( $this->execute_JSON_post_request( $this->get_request_url($params), $data ) ); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to create customer: ' . $e->getMessage() ); + return json_decode( $this->executeJsonPostRequest( $this->getRequestUrl($params), $data ) ); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } @@ -65,54 +61,51 @@ public function create( $data, $params = null ) { /** * Update a Customer in your organization * - * @param string $id - * @param $data Array of properties and property values for customer - * @param $params Array of optional query parameters - * - * @return Response body from HTTP POST request - * - * @throws Facturapi_Exception + * @param string $id Customer ID. + * @param array $data Customer data. + * @param array|null $params Optional query parameters. + * @return mixed JSON-decoded response. * + * @throws FacturapiException */ - public function update( $id, $data, $params = null ) { + public function update( $id, $data, $params = null ): mixed { try { - return json_decode( $this->execute_JSON_put_request( $this->get_request_url( $id, $params ), $data ) ); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to update customer: ' . $e->getMessage() ); + return json_decode( $this->executeJsonPutRequest( $this->getRequestUrl( $id, $params ), $data ) ); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** * Delete a Customer in your organization * - * @param string $id : Unique ID for the customer - * - * @return Response body from HTTP POST request + * @param string $id Customer ID. + * @return mixed JSON-decoded response. * - * @throws Facturapi_Exception - **/ - public function delete( $id ) { + * @throws FacturapiException + */ + public function delete( $id ): mixed { try { - return json_decode( $this->execute_delete_request( $this->get_request_url( $id ), null ) ); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to delete customer: ' . $e->getMessage() ); + return json_decode( $this->executeDeleteRequest( $this->getRequestUrl( $id ), null ) ); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } - + /** * Validates that a Customer's tax info is still valid * - * @param string $id : Unique ID for the customer - * @return Response body from HTTP POST request + * @param string $id Customer ID. + * @return mixed JSON-decoded response. * - * @throws Facturapi_Exception - **/ - public function validateTaxInfo( $id ) { + * @throws FacturapiException + */ + public function validateTaxInfo( $id ): mixed { try { - return json_decode( $this->execute_get_request( $this->get_request_url( $id ) . "/tax-info-validation" ) ); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to validate customer\'s tax info: ' . $e->getMessage() ); + return json_decode( $this->executeGetRequest( $this->getRequestUrl( $id ) . "/tax-info-validation" ) ); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } -} \ No newline at end of file +} diff --git a/src/Resources/Invoices.php b/src/Resources/Invoices.php index 88541e8..1249979 100644 --- a/src/Resources/Invoices.php +++ b/src/Resources/Invoices.php @@ -3,235 +3,333 @@ namespace Facturapi\Resources; use Facturapi\Http\BaseClient; -use Facturapi\Exceptions\Facturapi_Exception; +use Facturapi\Exceptions\FacturapiException; class Invoices extends BaseClient { - protected $ENDPOINT = 'invoices'; + protected string $ENDPOINT = 'invoices'; /** * Get all Invoices - * @param query Array of query parameters for the search - * @return JSON objects for all Invoices - * @throws Facturapi_Exception - **/ - public function all( $query = null ) { + * + * @param array|null $query Query parameters. + * @return mixed JSON-decoded response. + * @throws FacturapiException + */ + public function all( $query = null ): mixed { try { - return json_decode( $this->execute_get_request( $this->get_request_url( $query ) ) ); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to get Invoices: ' . $e->getMessage() ); + return json_decode( $this->executeGetRequest( $this->getRequestUrl( $query ) ) ); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** * Get an Invoice by ID - * @param id : Unique ID for Invoice - * @return JSON object for requested Invoice - * @throws Facturapi_Exception - **/ - public function retrieve( $id ) { + * + * @param string $id Invoice ID. + * @return mixed JSON-decoded response. + * @throws FacturapiException + */ + public function retrieve( $id ): mixed { try { - return json_decode( $this->execute_get_request( $this->get_request_url( $id ) ) ); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to get Invoice: ' . $e->getMessage()); + return json_decode( $this->executeGetRequest( $this->getRequestUrl( $id ) ) ); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** * Create an Invoice in your organization * - * @param body : array of properties and property values for new Invoice - * @param query : array of query parameters - * @return Response body with JSON object - * for created Invoice from HTTP POST request - * @throws Facturapi_Exception - **/ - public function create( $body, $query = null) { + * @param array $body Invoice payload. + * @param array|null $query Query parameters. + * @return mixed JSON-decoded response. + * @throws FacturapiException + */ + public function create( $body, $query = null): mixed { try { - return json_decode( $this->execute_JSON_post_request( $this->get_request_url($query), $body) ); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to create Invoice: ' . $e->getMessage()); + return json_decode( $this->executeJsonPostRequest( $this->getRequestUrl($query), $body) ); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** * Cancel an Invoice in your organization - * @param id : Unique ID for the Invoice - * @param query URL query params - * @return Response body from HTTP POST request - * @throws Facturapi_Exception - **/ - public function cancel( $id, $query ) { + * + * @param string $id Invoice ID. + * @param array $query URL query parameters. + * @return mixed JSON-decoded response. + * @throws FacturapiException + */ + public function cancel( $id, $query ): mixed { try { - return json_decode( $this->execute_delete_request( $this->get_request_url( $id, $query ), null ) ); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to cancel Invoice: ' . $e->getMessage()); + return json_decode( $this->executeDeleteRequest( $this->getRequestUrl( $id, $query ), null ) ); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** * Sends the invoice to the customer's email - * @param id : Unique ID for Invoice - * @return JSON object for requested Invoice - * @throws Facturapi_Exception - **/ - public function send_by_email( $id, $email = null ) { + * + * @param string $id Invoice ID. + * @param string|array|null $email Email or list of emails. + * @return mixed JSON-decoded response. + * @throws FacturapiException + */ + public function sendByEmail( $id, $email = null ): mixed { try { - return json_decode( $this->execute_JSON_post_request( - $this->get_request_url($id) . "/email", + return json_decode( $this->executeJsonPostRequest( + $this->getRequestUrl($id) . "/email", array("email" => $email) )); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to send Invoice: ' . $e->getMessage()); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } + /** + * @deprecated Use sendByEmail() instead. Will be removed in v5. + */ + public function send_by_email( $id, $email = null ): mixed { + trigger_error('Invoices::send_by_email() is deprecated and will be removed in v5. Use sendByEmail() instead.', E_USER_DEPRECATED); + return $this->sendByEmail( $id, $email ); + } + /** * Downloads the specified invoice in a ZIP package containing both PDF and XML files - * @param id : Unique ID for Invoice - * @return ZIP file in a stream - * @throws Facturapi_Exception - **/ - public function download_zip( $id ) { + * + * @param string $id Invoice ID. + * @return string ZIP file contents. + * @throws FacturapiException + */ + public function downloadZip( $id ): string { try { - return $this->execute_get_request( $this->get_request_url( $id ) . "/zip" ); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to download ZIP file: ' . $e->getMessage()); + return $this->executeGetRequest( $this->getRequestUrl( $id ) . "/zip" ); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } + /** + * @deprecated Use downloadZip() instead. Will be removed in v5. + */ + public function download_zip( $id ): string { + trigger_error('Invoices::download_zip() is deprecated and will be removed in v5. Use downloadZip() instead.', E_USER_DEPRECATED); + return $this->downloadZip( $id ); + } + /** * Downloads the specified invoice in a PDF file - * @param id : Unique ID for Invoice - * @return PDF file in a stream - * @throws Facturapi_Exception - **/ - public function download_pdf( $id ) { + * + * @param string $id Invoice ID. + * @return string Raw PDF bytes (binary string, not base64-encoded). + * @throws FacturapiException + */ + public function downloadPdf( $id ): string { try { - return $this->execute_get_request( $this->get_request_url( $id ) . "/pdf" ); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to download PDF file: ' . $e->getMessage()); + return $this->executeGetRequest( $this->getRequestUrl( $id ) . "/pdf" ); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } + /** + * @deprecated Use downloadPdf() instead. Will be removed in v5. + */ + public function download_pdf( $id ): string { + trigger_error('Invoices::download_pdf() is deprecated and will be removed in v5. Use downloadPdf() instead.', E_USER_DEPRECATED); + return $this->downloadPdf( $id ); + } + /** * Downloads the specified invoice in a XML file - * @param id : Unique ID for Invoice - * @return XML file in a stream - * @throws Facturapi_Exception - **/ - public function download_xml( $id ) { + * + * @param string $id Invoice ID. + * @return string XML file contents. + * @throws FacturapiException + */ + public function downloadXml( $id ): string { try { - return $this->execute_get_request( $this->get_request_url( $id ) . "/xml" ); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to download XML file: ' . $e->getMessage()); + return $this->executeGetRequest( $this->getRequestUrl( $id ) . "/xml" ); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } + /** + * @deprecated Use downloadXml() instead. Will be removed in v5. + */ + public function download_xml( $id ): string { + trigger_error('Invoices::download_xml() is deprecated and will be removed in v5. Use downloadXml() instead.', E_USER_DEPRECATED); + return $this->downloadXml( $id ); + } + /** * Downloads the cancellation receipt of a canceled invoice in XML format - * @param id : Unique ID for Invoice - * @return XML file in a stream - * @throws Facturapi_Exception - **/ - public function download_cancellation_receipt_xml( $id ) { + * + * @param string $id Invoice ID. + * @return string XML file contents. + * @throws FacturapiException + */ + public function downloadCancellationReceiptXml( $id ): string { try { - return $this->execute_get_request( $this->get_request_url( $id . "/cancellation_receipt/xml" ) ); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to download cancellation receipt: ' . $e->getMessage()); + return $this->executeGetRequest( $this->getRequestUrl( $id . "/cancellation_receipt/xml" ) ); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } - /** + /** + * @deprecated Use downloadCancellationReceiptXml() instead. Will be removed in v5. + */ + public function download_cancellation_receipt_xml( $id ): string { + trigger_error('Invoices::download_cancellation_receipt_xml() is deprecated and will be removed in v5. Use downloadCancellationReceiptXml() instead.', E_USER_DEPRECATED); + return $this->downloadCancellationReceiptXml( $id ); + } + + /** * Downloads the cancellation receipt of a canceled invoice in PDF format * - * @param id : Unique ID for Invoice - * - * @return XML file in a stream + * @param string $id Invoice ID. + * @return string PDF file contents. * - * @throws Facturapi_Exception - **/ - public function download_cancellation_receipt_pdf( $id ) { + * @throws FacturapiException + */ + public function downloadCancellationReceiptPdf( $id ): string { try { - return $this->execute_get_request( $this->get_request_url( $id ) . "/cancellation_receipt/pdf" ); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to download cancellation receipt: ' . $e->getMessage()); + return $this->executeGetRequest( $this->getRequestUrl( $id ) . "/cancellation_receipt/pdf" ); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } + /** + * @deprecated Use downloadCancellationReceiptPdf() instead. Will be removed in v5. + */ + public function download_cancellation_receipt_pdf( $id ): string { + trigger_error('Invoices::download_cancellation_receipt_pdf() is deprecated and will be removed in v5. Use downloadCancellationReceiptPdf() instead.', E_USER_DEPRECATED); + return $this->downloadCancellationReceiptPdf( $id ); + } + /** * Updates the status of an Invoice with the latest information from the SAT. * In Test mode, this method will simulate a status update to a "canceled" status. - * @param id : Unique ID for Invoice - * @return JSON Updated Invoice object - * @throws Facturapi_Exception + * + * @param string $id Invoice ID. + * @return mixed JSON-decoded response. + * @throws FacturapiException */ - public function update_status( $id ) { + public function updateStatus( $id ): mixed { try { - return json_decode( $this->execute_JSON_put_request( $this->get_request_url( $id . "/status" ), null ) ); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to update status: ' . $e ); + return json_decode( $this->executeJsonPutRequest( $this->getRequestUrl( $id . "/status" ), null ) ); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } + /** + * @deprecated Use updateStatus() instead. Will be removed in v5. + */ + public function update_status( $id ): mixed { + trigger_error('Invoices::update_status() is deprecated and will be removed in v5. Use updateStatus() instead.', E_USER_DEPRECATED); + return $this->updateStatus( $id ); + } + /** * Updates an Invoice with "draft" status - * - * @param id : Unique ID for Invoice - * @param body : array of properties and property values for the fields to edit - * @return JSON Edited draft Invoice object - * @throws Facturapi_Exception + * + * @param string $id Invoice ID. + * @param array $body Draft payload. + * @return mixed JSON-decoded response. + * @throws FacturapiException */ - public function update_draft( $id, $body ) { + public function updateDraft( $id, $body ): mixed { try { - return json_decode( $this->execute_JSON_put_request( $this->get_request_url( $id ), $body ) ); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to edit draft: ' . $e ); + return json_decode( $this->executeJsonPutRequest( $this->getRequestUrl( $id ), $body ) ); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } + /** + * @deprecated Use updateDraft() instead. Will be removed in v5. + */ + public function update_draft( $id, $body ): mixed { + trigger_error('Invoices::update_draft() is deprecated and will be removed in v5. Use updateDraft() instead.', E_USER_DEPRECATED); + return $this->updateDraft( $id, $body ); + } + /** * Stamps a draft invoice - * - * @param id : Unique ID for Invoice - * @param query : URL query params - * @return JSON Stamped Invoice object - * @throws Facturapi_Exception - */ - public function stamp_draft( $id, $query = null ) { + * + * @param string $id Invoice ID. + * @param array|null $query URL query parameters. + * @return mixed JSON-decoded response. + * @throws FacturapiException + */ + public function stampDraft( $id, $query = null ): mixed { try { - return json_decode( $this->execute_JSON_post_request( $this->get_request_url( $id . "/stamp", $query ) ) ); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to stamp draft: ' . $e ); + return json_decode( $this->executeJsonPostRequest( $this->getRequestUrl( $id . "/stamp", $query ) ) ); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } + /** + * @deprecated Use stampDraft() instead. Will be removed in v5. + */ + public function stamp_draft( $id, $query = null ): mixed { + trigger_error('Invoices::stamp_draft() is deprecated and will be removed in v5. Use stampDraft() instead.', E_USER_DEPRECATED); + return $this->stampDraft( $id, $query ); + } + /** * Creates a new draft Invoice copying the information from the specified Invoice - * @param id : Unique ID for Invoice - * @return JSON Copied draft Invoice object - * @throws Facturapi_Exception + * + * @param string $id Invoice ID. + * @return mixed JSON-decoded response. + * @throws FacturapiException */ - public function copy_to_draft( $id ) { + public function copyToDraft( $id ): mixed { try { - return json_decode( $this->execute_JSON_post_request( $this->get_request_url( $id . "/copy" ), null ) ); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to copy draft: ' . $e ); + return json_decode( $this->executeJsonPostRequest( $this->getRequestUrl( $id . "/copy" ), null ) ); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } + /** + * @deprecated Use copyToDraft() instead. Will be removed in v5. + */ + public function copy_to_draft( $id ): mixed { + trigger_error('Invoices::copy_to_draft() is deprecated and will be removed in v5. Use copyToDraft() instead.', E_USER_DEPRECATED); + return $this->copyToDraft( $id ); + } + /** * Generates a preview of an invoice in PDF format without stamping it or saving it - * @param array $body Array of properties and property values for new Invoice - * @return string PDF file contents - * @throws Facturapi_Exception - **/ - public function preview_pdf( $body ) { + * + * @param array $body Invoice payload. + * @return string PDF file contents. + * @throws FacturapiException + */ + public function previewPdf( $body ): string { try { - return $this->execute_JSON_post_request( $this->get_request_url( "preview/pdf" ), $body ); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to generate PDF preview: ' . $e->getMessage()); + return $this->executeJsonPostRequest( $this->getRequestUrl( "preview/pdf" ), $body ); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } -} \ No newline at end of file + + /** + * @deprecated Use previewPdf() instead. Will be removed in v5. + */ + public function preview_pdf( $body ): string { + trigger_error('Invoices::preview_pdf() is deprecated and will be removed in v5. Use previewPdf() instead.', E_USER_DEPRECATED); + return $this->previewPdf( $body ); + } +} diff --git a/src/Resources/Organizations.php b/src/Resources/Organizations.php index 42ab8c6..56621f8 100644 --- a/src/Resources/Organizations.php +++ b/src/Resources/Organizations.php @@ -3,462 +3,434 @@ namespace Facturapi\Resources; use Facturapi\Http\BaseClient; -use Facturapi\Exceptions\Facturapi_Exception; +use Facturapi\Exceptions\FacturapiException; class Organizations extends BaseClient { - protected $ENDPOINT = 'organizations'; + protected string $ENDPOINT = 'organizations'; /** * Get all Organizations * - * @param Search parameters + * @param array|null $params Search parameters. + * @return mixed JSON-decoded response. * - * @return JSON objects for all Organizations - * - * @throws Facturapi_Exception - **/ - public function all($params = null) + * @throws FacturapiException + */ + public function all($params = null): mixed { try { - return json_decode($this->execute_get_request($this->get_request_url($params))); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to get organizations: ' . $e->getMessage()); + return json_decode($this->executeGetRequest($this->getRequestUrl($params))); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** - * Get a Organization by ID + * Get an Organization by ID. * - * @param id : Unique ID for Organization + * @param string $id Organization ID. + * @return mixed JSON-decoded response. * - * @return JSON object for requested Organization - * - * @throws Facturapi_Exception - **/ - public function retrieve($id) + * @throws FacturapiException + */ + public function retrieve($id): mixed { try { - return json_decode($this->execute_get_request($this->get_request_url($id))); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to get organization: ' . $e->getMessage()); + return json_decode($this->executeGetRequest($this->getRequestUrl($id))); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** - * Create a Organization + * Create an Organization. * - * @param params : array of properties and property values for new Organization + * @param array $params Organization payload. + * @return mixed JSON-decoded response. * - * @return Response body with JSON object - * for created Organization from HTTP POST request - * - * @throws Facturapi_Exception - **/ - public function create($params) + * @throws FacturapiException + */ + public function create($params): mixed { try { - return json_decode($this->execute_JSON_post_request($this->get_request_url(), $params)); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to create organization: ' . $e->getMessage()); + return json_decode($this->executeJsonPostRequest($this->getRequestUrl(), $params)); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** - * Update a Organization's legal information - * - * @param $id - * @param $params array of properties and property values for Organization's legal information + * Update an organization's legal information. * - * @return Response body from HTTP PUT request - * - * @throws Facturapi_Exception + * @param string $id Organization ID. + * @param array $params Legal information payload. + * @return mixed JSON-decoded response. * + * @throws FacturapiException */ - public function updateLegal($id, $params) + public function updateLegal($id, $params): mixed { try { - return json_decode($this->execute_JSON_put_request($this->get_request_url($id) . "/legal", $params)); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to update organization\'s legal information: ' . $e->getMessage()); + return json_decode($this->executeJsonPutRequest($this->getRequestUrl($id) . "/legal", $params)); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** - * Update a Organization's customization information - * - * @param $id - * @param $params array of properties and property values for Organization's customization information - * - * @return Response body from HTTP PUT request + * Update an organization's customization information. * - * @throws Facturapi_Exception + * @param string $id Organization ID. + * @param array $params Customization payload. + * @return mixed JSON-decoded response. * + * @throws FacturapiException */ - public function updateCustomization($id, $params) + public function updateCustomization($id, $params): mixed { try { - return json_decode($this->execute_JSON_put_request($this->get_request_url($id) . "/customization", $params)); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to update organization\'s customization information: ' . $e->getMessage()); + return json_decode($this->executeJsonPutRequest($this->getRequestUrl($id) . "/customization", $params)); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** - * Update a Organization's receipt settings + * Update an organization's receipt settings. * - * @param $id - * @param $params array of properties and property values for Organization's receipt settings - * - * @return Response body from HTTP PUT request - * - * @throws Facturapi_Exception + * @param string $id Organization ID. + * @param array $params Receipt settings payload. + * @return mixed JSON-decoded response. * + * @throws FacturapiException */ - public function updateReceiptSettings($id, $params) + public function updateReceiptSettings($id, $params): mixed { try { - return json_decode($this->execute_JSON_put_request($this->get_request_url($id) . "/receipts", $params)); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to update organization\'s receipt settings: ' . $e->getMessage()); + return json_decode($this->executeJsonPutRequest($this->getRequestUrl($id) . "/receipts", $params)); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** - * Update the Organization's domain - * - * @param $id Organization Id - * @param $params array of properties and property values for the Organization's domain + * Update an organization's domain. * - * @return Response body from HTTP PUT request - * - * @throws Facturapi_Exception + * @param string $id Organization ID. + * @param array $params Domain payload. + * @return mixed JSON-decoded response. * + * @throws FacturapiException */ - public function updateDomain($id, $params) + public function updateDomain($id, $params): mixed { try { - return json_decode($this->execute_JSON_put_request($this->get_request_url($id) . "/domain", $params)); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to update organization\'s domain: ' . $e->getMessage()); + return json_decode($this->executeJsonPutRequest($this->getRequestUrl($id) . "/domain", $params)); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** - * Update the Organization's domain - * - * @param $id Organization Id - * @param $params array of properties and property values for the Organization's domain + * Check domain availability. * - * @return Response body from HTTP PUT request - * - * @throws Facturapi_Exception + * @param string $id Organization ID. + * @param array $params Domain check parameters. + * @return mixed JSON-decoded response. * + * @throws FacturapiException */ - public function checkDomainIsAvailable($id, $params) + public function checkDomainIsAvailable($id, $params): mixed { try { return json_decode( - $this->execute_get_request( - $this->get_request_url("domain-check" . $this->array_to_params($params)) + $this->executeGetRequest( + $this->getRequestUrl("domain-check", $params) ) ); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to check domain\'s availability: ' . $e->getMessage()); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** * Uploads the organization's logo * - * @param $id - * @param $params array of properties and property values for Organization's logo - * - * @return Response body from HTTP PUT request - * - * @throws Facturapi_Exception + * @param string $id Organization ID. + * @param string $params Logo file path. + * @return mixed JSON-decoded response. * + * @throws FacturapiException */ - public function uploadLogo($id, $params) + public function uploadLogo($id, $params): mixed { try { - return json_decode($this->execute_data_put_request($this->get_request_url($id) . "/logo", $params)); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to upload organization\'s logo: ' . $e->getMessage()); + return json_decode($this->executeDataPutRequest($this->getRequestUrl($id) . "/logo", $params)); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** * Uploads the organization's certificate (CSD) * - * @param $id - * @param $params array of properties and property values for organization's certificate (CSD) - * - * @return Response body from HTTP PUT request - * - * @throws Facturapi_Exception + * @param string $id Organization ID. + * @param array $params Certificate payload (`cerFile`, `keyFile`, `password`). + * @return mixed JSON-decoded response. * + * @throws FacturapiException */ - public function uploadCertificate($id, $params) + public function uploadCertificate($id, $params): mixed { try { - return json_decode($this->execute_data_put_request($this->get_request_url($id) . "/certificate", $params)); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to upload organization\'s certificate (CSD): ' . $e->getMessage()); + return json_decode($this->executeDataPutRequest($this->getRequestUrl($id) . "/certificate", $params)); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** - * Get the Test Api Key for an Organization + * Get the test API key for an organization. * - * @param id : Unique ID for Organization + * @param string $id Organization ID. + * @return mixed JSON-decoded response. * - * @return String Test Api Key - * - * @throws Facturapi_Exception - **/ - public function getTestApiKey($id) + * @throws FacturapiException + */ + public function getTestApiKey($id): mixed { try { - return json_decode($this->execute_get_request($this->get_request_url($id) . "/apikeys/test")); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to get organization\'s test key: ' . $e->getMessage()); + return json_decode($this->executeGetRequest($this->getRequestUrl($id) . "/apikeys/test")); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** - * Renews the Test Api Key for an Organization and makes the previous one inactive - * - * @param id : Unique ID for Organization + * Renew the test API key for an organization. * - * @return String Test Api Key + * @param string $id Organization ID. + * @return mixed JSON-decoded response. * - * @throws Facturapi_Exception - **/ - public function renewTestApiKey($id) + * @throws FacturapiException + */ + public function renewTestApiKey($id): mixed { try { return json_decode( - $this->execute_JSON_put_request( - $this->get_request_url($id) . "/apikeys/test", + $this->executeJsonPutRequest( + $this->getRequestUrl($id) . "/apikeys/test", [] ) ); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to renew organization\'s test key: ' . $e->getMessage()); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** - * Renews the Test Api Key for an Organization and makes the previous one inactive + * @deprecated Use listLiveApiKeys() instead. Will be removed in v5. * - * @param id : Unique ID for Organization + * @param string $id Organization ID. + * @return mixed JSON-decoded response. * - * @return Array Array of object with first_12 characters, created_at field and id field + * @throws FacturapiException + */ + public function lisLiveApiKeys($id): mixed + { + trigger_error('Organizations::lisLiveApiKeys() is deprecated and will be removed in v5. Use listLiveApiKeys() instead.', E_USER_DEPRECATED); + return $this->listLiveApiKeys($id); + } + + /** + * List live API keys for an organization. + * + * @param string $id Organization ID. + * @return mixed JSON-decoded response. * - * @throws Facturapi_Exception - **/ - public function lisLiveApiKeys($id) + * @throws FacturapiException + */ + public function listLiveApiKeys($id): mixed { try { return json_decode( - $this->execute_get_request( - $this->get_request_url($id) . "/apikeys/live", - [] + $this->executeGetRequest( + $this->getRequestUrl($id) . "/apikeys/live" ) ); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception($e); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } - /** - * Renews the Test Api Key for an Organization and makes the previous one inactive + * Renew the live API key for an organization. * - * @param id : Unique ID for Organization + * @param string $id Organization ID. + * @return mixed JSON-decoded response. * - * @return String Live Api Key - * - * @throws Facturapi_Exception - **/ - public function renewLiveApiKey($id) + * @throws FacturapiException + */ + public function renewLiveApiKey($id): mixed { try { return json_decode( - $this->execute_JSON_put_request( - $this->get_request_url($id) . "/apikeys/live", + $this->executeJsonPutRequest( + $this->getRequestUrl($id) . "/apikeys/live", [] ) ); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to renew organization\'s live key: ' . $e->getMessage()); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** - * Deletes the Test Api Key for an Organization and makes the previous one inactive - * - * @param organizationId : Unique ID for Organization - * @param apiKeyId: Unique ID for the Api Key + * Delete one live API key for an organization. * - * @return Array Array of object with first_12 characters, created_at field and id field + * @param string $organizationId Organization ID. + * @param string $apiKeyId API key ID. + * @return mixed JSON-decoded response. * - * @throws Facturapi_Exception - **/ - public function deleteLiveApiKey($organizationId, $apiKeyId) + * @throws FacturapiException + */ + public function deleteLiveApiKey($organizationId, $apiKeyId): mixed { try { return json_decode( - $this->execute_JSON_put_request( - $this->get_request_url($organizationId) . "/apikeys/live" . "/" . $apiKeyId, + $this->executeJsonPutRequest( + $this->getRequestUrl($organizationId) . "/apikeys/live" . "/" . $apiKeyId, [] ) ); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception($e); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** - * Delete a Organization - * - * @param id : Unique ID for the Organization + * Delete an organization. * - * @return Response body from HTTP DELETE request + * @param string $id Organization ID. + * @return mixed JSON-decoded response. * - * @throws Facturapi_Exception - **/ - public function delete($id) + * @throws FacturapiException + */ + public function delete($id): mixed { try { - return json_decode($this->execute_delete_request($this->get_request_url($id), null)); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to delete organization: ' . $e->getMessage()); + return json_decode($this->executeDeleteRequest($this->getRequestUrl($id), null)); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** - * Delete a Organization's Certificate + * Delete an organization's certificate. * - * @param id : Unique ID for the Organization + * @param string $id Organization ID. + * @return mixed JSON-decoded response. * - * @return Response body from HTTP DELETE request - * - * @throws Facturapi_Exception - **/ - public function deleteCertificate($id) + * @throws FacturapiException + */ + public function deleteCertificate($id): mixed { try { - return json_decode($this->execute_delete_request($this->get_request_url($id) . "/certificate", null)); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to delete organization: ' . $e->getMessage()); + return json_decode($this->executeDeleteRequest($this->getRequestUrl($id) . "/certificate", null)); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** - * Get the series of an Organization - * - * @param id : Unique ID for the Organization + * Get series groups for an organization. * - * @return Response body from HTTP GET request + * @param string $id Organization ID. + * @return mixed JSON-decoded response. * - * @throws Facturapi_Exception - **/ - public function getSeriesGroup($id) + * @throws FacturapiException + */ + public function getSeriesGroup($id): mixed { try { - return json_decode($this->execute_get_request($this->get_request_url($id) . "/series-group")); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to find series: ' . $e); + return json_decode($this->executeGetRequest($this->getRequestUrl($id) . "/series-group")); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** - * Create a Series Organization - * - * @param id : Unique ID for the Organization - * - * - * @param params : object of properties and property values for new Series Organization + * Create a series group for an organization. * - * @return Response body with JSON object - * for created Organization from HTTP POST request + * @param string $id Organization ID. + * @param array $params Series group payload. + * @return mixed JSON-decoded response. * - * @throws Facturapi_Exception - **/ - public function createSeriesGroup($id, $params) + * @throws FacturapiException + */ + public function createSeriesGroup($id, $params): mixed { try { - return json_decode($this->execute_JSON_post_request($this->get_request_url($id) . "/series-group", $params)); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to create series: ' . $e); + return json_decode($this->executeJsonPostRequest($this->getRequestUrl($id) . "/series-group", $params)); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** - * Update a Series Organization - * - * @param id : Unique ID for the Organization - * - * @param series_name: Name of the series to update + * Update a series group for an organization. * - * @param params : object of properties and property values for updated Series Organization + * @param string $id Organization ID. + * @param string $series_name Series name. + * @param array $params Series group payload. + * @return mixed JSON-decoded response. * - * @return Response body with JSON object - * for updated Series Organization from HTTP POST request - * - * @throws Facturapi_Exception - **/ - public function updateSeriesGroup($id, $series_name, $params) + * @throws FacturapiException + */ + public function updateSeriesGroup($id, $series_name, $params): mixed { try { - return json_decode($this->execute_JSON_put_request($this->get_request_url($id) . "/series-group" . "/" . $series_name, $params)); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to create series: ' . $e); + return json_decode($this->executeJsonPutRequest($this->getRequestUrl($id) . "/series-group" . "/" . $series_name, $params)); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** - * Delete a Series Organization - * - * @param id : Unique ID for the Organization - * - * @param series_name: Name of the series to update - * - * @param params : object of properties and property values for new Series Organization + * Delete a series group for an organization. * - * @return Response body with JSON object - * for delete Series Organization from HTTP DELETE request + * @param string $id Organization ID. + * @param string $series_name Series name. + * @return mixed JSON-decoded response. * - * @throws Facturapi_Exception - **/ - public function deleteSeriesGroup($id, $series_name) + * @throws FacturapiException + */ + public function deleteSeriesGroup($id, $series_name): mixed { try { - return json_decode($this->execute_delete_request($this->get_request_url($id) . "/series-group" . "/" . $series_name, null)); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to create series: ' . $e); + return json_decode($this->executeDeleteRequest($this->getRequestUrl($id) . "/series-group" . "/" . $series_name, null)); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** * Update self-invoice settings for an Organization * - * @param string $id : Unique ID for the Organization - * @param array $params : Properties and values for self-invoice settings - * - * @return Response body from HTTP PUT request + * @param string $id Organization ID. + * @param array $params Self-invoice settings payload. + * @return mixed JSON-decoded response. * - * @throws Facturapi_Exception - **/ - public function updateSelfInvoiceSettings($id, $params) + * @throws FacturapiException + */ + public function updateSelfInvoiceSettings($id, $params): mixed { try { - return json_decode($this->execute_JSON_put_request($this->get_request_url($id) . "/self-invoice", $params)); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to update organization\'s self-invoice settings: ' . $e->getMessage()); + return json_decode($this->executeJsonPutRequest($this->getRequestUrl($id) . "/self-invoice", $params)); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } } diff --git a/src/Resources/Products.php b/src/Resources/Products.php index 0921c9b..0144e6b 100644 --- a/src/Resources/Products.php +++ b/src/Resources/Products.php @@ -3,61 +3,57 @@ namespace Facturapi\Resources; use Facturapi\Http\BaseClient; -use Facturapi\Exceptions\Facturapi_Exception; +use Facturapi\Exceptions\FacturapiException; class Products extends BaseClient { - protected $ENDPOINT = 'products'; + protected string $ENDPOINT = 'products'; /** * Get all Products * - * @param Search parameters + * @param array|null $params Search parameters. + * @return mixed JSON-decoded response. * - * @return JSON objects for all Products - * - * @throws Facturapi_Exception - **/ - public function all( $params = null ) { + * @throws FacturapiException + */ + public function all( $params = null ): mixed { try { - return json_decode( $this->execute_get_request( $this->get_request_url( $params ) ) ); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to get products: ' . $e->getMessage() ); + return json_decode( $this->executeGetRequest( $this->getRequestUrl( $params ) ) ); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** * Get a Product by ID * - * @param id : Unique ID for Product - * - * @return JSON object for requested Product + * @param string $id Product ID. + * @return mixed JSON-decoded response. * - * @throws Facturapi_Exception - **/ - public function retrieve( $id ) { + * @throws FacturapiException + */ + public function retrieve( $id ): mixed { try { - return json_decode( $this->execute_get_request( $this->get_request_url( $id ) ) ); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to get product: ' . $e->getMessage() ); + return json_decode( $this->executeGetRequest( $this->getRequestUrl( $id ) ) ); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** * Create a Product in your organization * - * @param params : array of properties and property values for new Product - * - * @return Response body with JSON object - * for created Product from HTTP POST request + * @param array $params Product data. + * @return mixed JSON-decoded response. * - * @throws Facturapi_Exception - **/ - public function create( $params ) { + * @throws FacturapiException + */ + public function create( $params ): mixed { try { - return json_decode( $this->execute_JSON_post_request( $this->get_request_url(), $params ) ); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to create product: ' . $e->getMessage() ); + return json_decode( $this->executeJsonPostRequest( $this->getRequestUrl(), $params ) ); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } @@ -65,37 +61,34 @@ public function create( $params ) { /** * Update a Product in your organization * - * @param $id - * @param $params array of properties and property values for Product - * - * @return Response body from HTTP POST request - * - * @throws Facturapi_Exception + * @param string $id Product ID. + * @param array $params Product data. + * @return mixed JSON-decoded response. * + * @throws FacturapiException */ - public function update( $id, $params ) { + public function update( $id, $params ): mixed { try { - return json_decode( $this->execute_JSON_put_request( $this->get_request_url( $id ), $params ) ); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to update product: ' . $e->getMessage() ); + return json_decode( $this->executeJsonPutRequest( $this->getRequestUrl( $id ), $params ) ); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** * Delete a Product in your organization * - * @param id : Unique ID for the Product + * @param string $id Product ID. + * @return mixed JSON-decoded response. * - * @return Response body from HTTP POST request - * - * @throws Facturapi_Exception - **/ - public function delete( $id ) { + * @throws FacturapiException + */ + public function delete( $id ): mixed { try { - return json_decode( $this->execute_delete_request( $this->get_request_url( $id ), null ) ); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to delete product: ' . $e->getMessage() ); + return json_decode( $this->executeDeleteRequest( $this->getRequestUrl( $id ), null ) ); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } -} \ No newline at end of file +} diff --git a/src/Resources/Receipts.php b/src/Resources/Receipts.php index 6913854..af7f8a2 100644 --- a/src/Resources/Receipts.php +++ b/src/Resources/Receipts.php @@ -3,60 +3,57 @@ namespace Facturapi\Resources; use Facturapi\Http\BaseClient; -use Facturapi\Exceptions\Facturapi_Exception; +use Facturapi\Exceptions\FacturapiException; class Receipts extends BaseClient { - protected $ENDPOINT = 'receipts'; + protected string $ENDPOINT = 'receipts'; /** * Search or list all receipts in your organization * - * @param $params Search parameters - * - * @return JSON a receipt search result object + * @param array|null $params Search parameters. + * @return mixed JSON-decoded response. * - * @throws Facturapi_Exception - **/ - public function all( $params = null ) { + * @throws FacturapiException + */ + public function all( $params = null ): mixed { try { - return json_decode( $this->execute_get_request( $this->get_request_url( $params ) ) ); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to get receipts: ' . $e->getMessage() ); + return json_decode( $this->executeGetRequest( $this->getRequestUrl( $params ) ) ); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** * Get a Receipt by ID * - * @param id : Unique ID for Receipt - * - * @return JSON object for requested Receipt + * @param string $id Receipt ID. + * @return mixed JSON-decoded response. * - * @throws Facturapi_Exception - **/ - public function retrieve( $id ) { + * @throws FacturapiException + */ + public function retrieve( $id ): mixed { try { - return json_decode( $this->execute_get_request( $this->get_request_url( $id ) ) ); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to get receipt: ' . $e->getMessage() ); + return json_decode( $this->executeGetRequest( $this->getRequestUrl( $id ) ) ); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** * Create a Receipt in your organization * - * @param params : array of properties and property values for new Receipt + * @param array $params Receipt payload. + * @return mixed JSON-decoded response. * - * @return Response The Receipt object we just created - * - * @throws Facturapi_Exception - **/ - public function create( $params ) { + * @throws FacturapiException + */ + public function create( $params ): mixed { try { - return json_decode( $this->execute_JSON_post_request( $this->get_request_url(), $params ) ); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to create receipt: ' . $e->getMessage() ); + return json_decode( $this->executeJsonPostRequest( $this->getRequestUrl(), $params ) ); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } @@ -64,91 +61,101 @@ public function create( $params ) { /** * Creates an invoice for a Receipt * - * @param $id Receipt Id - * @param $params Array of properties and property values for invoicing a receipt - * - * @return Response The Invoice object of the invoice we just created - * - * @throws Facturapi_Exception + * @param string $id Receipt ID. + * @param array $params Invoice payload. + * @return mixed JSON-decoded response. * + * @throws FacturapiException */ - public function invoice( $id, $params ) { + public function invoice( $id, $params ): mixed { try { - return json_decode( $this->execute_JSON_post_request( $this->get_request_url( $id ) . "/invoice", $params ) ); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to invoice receipt: ' . $e->getMessage() ); + return json_decode( $this->executeJsonPostRequest( $this->getRequestUrl( $id ) . "/invoice", $params ) ); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } - + /** * Creates a global invoice from the open receipts in the last completed period * - * @param $params Array of properties and property values for creating a global invoice - * - * @return Response the Invoice object of the global invoice we just created - * - * @throws Facturapi_Exception + * @param array $params Global invoice payload. + * @return mixed JSON-decoded response. * + * @throws FacturapiException */ - public function createGlobalInvoice( $params ) { + public function createGlobalInvoice( $params ): mixed { try { - return json_decode( $this->execute_JSON_post_request( $this->get_request_url() . "/global-invoice", $params ) ); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to create global invoice: ' . $e->getMessage() ); + return json_decode( $this->executeJsonPostRequest( $this->getRequestUrl() . "/global-invoice", $params ) ); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** * Cancel a Receipt * - * @param $id : Unique ID for the Receipt - * - * @return Response the Receipt object + * @param string $id Receipt ID. + * @return mixed JSON-decoded response. * - * @throws Facturapi_Exception - **/ - public function cancel( $id ) { + * @throws FacturapiException + */ + public function cancel( $id ): mixed { try { - return json_decode( $this->execute_delete_request( $this->get_request_url( $id ), null ) ); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to cancel receipt: ' . $e->getMessage() ); + return json_decode( $this->executeDeleteRequest( $this->getRequestUrl( $id ), null ) ); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** * Sends the receipt to the customer's email * - * @param id : Unique ID for Receipt + * @param string $id Receipt ID. + * @param string|array|null $email Email or list of emails. + * @return mixed JSON-decoded response. * - * @return JSON object for requested Receipt - * - * @throws Facturapi_Exception - **/ - public function send_by_email( $id, $email = null ) { + * @throws FacturapiException + */ + public function sendByEmail( $id, $email = null ): mixed { try { - return json_decode( $this->execute_JSON_post_request( - $this->get_request_url($id) . "/email", + return json_decode( $this->executeJsonPostRequest( + $this->getRequestUrl($id) . "/email", $email == null ? null : array("email" => $email) )); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to send Receipt: ' . $e->getMessage() ); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } + /** + * @deprecated Use sendByEmail() instead. Will be removed in v5. + */ + public function send_by_email( $id, $email = null ): mixed { + trigger_error('Receipts::send_by_email() is deprecated and will be removed in v5. Use sendByEmail() instead.', E_USER_DEPRECATED); + return $this->sendByEmail( $id, $email ); + } + /** * Downloads the specified receipt in a PDF file * - * @param id : Unique ID for Receipt + * @param string $id Receipt ID. + * @return string Raw PDF bytes (binary string, not base64-encoded). * - * @return PDF file in a stream - * - * @throws Facturapi_Exception - **/ - public function download_pdf( $id ) { + * @throws FacturapiException + */ + public function downloadPdf( $id ): string { try { - return $this->execute_get_request( $this->get_request_url( $id ) . "/pdf" ); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Unable to download PDF file: ' . $e->getMessage() ); + return $this->executeGetRequest( $this->getRequestUrl( $id ) . "/pdf" ); + } catch ( FacturapiException $e ) { + throw new FacturapiException($e->getMessage(), 0, $e); } } -} \ No newline at end of file + + /** + * @deprecated Use downloadPdf() instead. Will be removed in v5. + */ + public function download_pdf( $id ): string { + trigger_error('Receipts::download_pdf() is deprecated and will be removed in v5. Use downloadPdf() instead.', E_USER_DEPRECATED); + return $this->downloadPdf( $id ); + } +} diff --git a/src/Resources/Retentions.php b/src/Resources/Retentions.php index 55aaacb..9bb9cc3 100644 --- a/src/Resources/Retentions.php +++ b/src/Resources/Retentions.php @@ -3,64 +3,61 @@ namespace Facturapi\Resources; use Facturapi\Http\BaseClient; -use Facturapi\Exceptions\Facturapi_Exception; +use Facturapi\Exceptions\FacturapiException; class Retentions extends BaseClient { - protected $ENDPOINT = 'retentions'; + protected string $ENDPOINT = 'retentions'; /** * Search Retentions * - * @param Search parameters + * @param array|null $params Search parameters. + * @return mixed JSON-decoded response. * - * @return JSON search result object - * - * @throws Facturapi_Exception - **/ - public function all($params = null) + * @throws FacturapiException + */ + public function all($params = null): mixed { try { - return json_decode($this->execute_get_request($this->get_request_url($params))); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to get Retentions: ' . $e->getMessage()); + return json_decode($this->executeGetRequest($this->getRequestUrl($params))); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** * Get a Retention by ID * - * @param id : Retention ID - * - * @return JSON Retention object + * @param string $id Retention ID. + * @return mixed JSON-decoded response. * - * @throws Facturapi_Exception - **/ - public function retrieve($id) + * @throws FacturapiException + */ + public function retrieve($id): mixed { try { - return json_decode($this->execute_get_request($this->get_request_url($id))); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to get Retention: ' . $e->getMessage()); + return json_decode($this->executeGetRequest($this->getRequestUrl($id))); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** * Creates a Retention for the organization * - * @param params : array of properties and property values for new Retention - * - * @return Response body with JSON object with created Retention + * @param array $params Retention payload. + * @return mixed JSON-decoded response. * - * @throws Facturapi_Exception - **/ - public function create($params) + * @throws FacturapiException + */ + public function create($params): mixed { try { - return json_decode($this->execute_JSON_post_request($this->get_request_url(), $params)); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to create Retention: ' . $e->getMessage()); + return json_decode($this->executeJsonPostRequest($this->getRequestUrl(), $params)); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } @@ -68,95 +65,126 @@ public function create($params) /** * Cancels a Retention * - * @param id : Retention ID - * @param query URL query params - * @return Response Updated Retention object + * @param string $id Retention ID. + * @param array $query URL query parameters. + * @return mixed JSON-decoded response. * - * @throws Facturapi_Exception - **/ - public function cancel($id, $query) + * @throws FacturapiException + */ + public function cancel($id, $query): mixed { try { - return json_decode($this->execute_delete_request($this->get_request_url($id, $query), null)); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to cancel Retention: ' . $e->getMessage()); + return json_decode($this->executeDeleteRequest($this->getRequestUrl($id, $query), null)); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** * Sends the retention to the customer's email * - * @param id : Retention ID - * - * @param email : String or array of strings with email address(es) + * @param string $id Retention ID. + * @param string|array|null $email Email or list of emails. + * @return mixed JSON-decoded response. * - * @return JSON Result object - * - * @throws Facturapi_Exception - **/ - public function send_by_email($id, $email = null) + * @throws FacturapiException + */ + public function sendByEmail($id, $email = null): mixed { try { - return json_decode($this->execute_JSON_post_request( - $this->get_request_url($id) . "/email", + return json_decode($this->executeJsonPostRequest( + $this->getRequestUrl($id) . "/email", array("email" => $email) )); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to send Retention: ' . $e->getMessage()); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } + /** + * @deprecated Use sendByEmail() instead. Will be removed in v5. + */ + public function send_by_email($id, $email = null): mixed + { + trigger_error('Retentions::send_by_email() is deprecated and will be removed in v5. Use sendByEmail() instead.', E_USER_DEPRECATED); + return $this->sendByEmail($id, $email); + } + /** * Downloads the specified Retention in a ZIP package containing both PDF and XML files * - * @param id : Retention ID - * - * @return ZIP file in a stream + * @param string $id Retention ID. + * @return string ZIP file contents. * - * @throws Facturapi_Exception - **/ - public function download_zip($id) + * @throws FacturapiException + */ + public function downloadZip($id): string { try { - return $this->execute_get_request($this->get_request_url($id) . "/zip"); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to download ZIP file: ' . $e->getMessage()); + return $this->executeGetRequest($this->getRequestUrl($id) . "/zip"); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } + /** + * @deprecated Use downloadZip() instead. Will be removed in v5. + */ + public function download_zip($id): string + { + trigger_error('Retentions::download_zip() is deprecated and will be removed in v5. Use downloadZip() instead.', E_USER_DEPRECATED); + return $this->downloadZip($id); + } + /** * Downloads the specified Retention in a PDF file * - * @param id : Retention ID - * - * @return PDF file in a stream + * @param string $id Retention ID. + * @return string Raw PDF bytes (binary string, not base64-encoded). * - * @throws Facturapi_Exception - **/ - public function download_pdf($id) + * @throws FacturapiException + */ + public function downloadPdf($id): string { try { - return $this->execute_get_request($this->get_request_url($id) . "/pdf"); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to download PDF file: ' . $e->getMessage()); + return $this->executeGetRequest($this->getRequestUrl($id) . "/pdf"); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } + /** + * @deprecated Use downloadPdf() instead. Will be removed in v5. + */ + public function download_pdf($id): string + { + trigger_error('Retentions::download_pdf() is deprecated and will be removed in v5. Use downloadPdf() instead.', E_USER_DEPRECATED); + return $this->downloadPdf($id); + } + /** * Downloads the specified Retention in a XML file * - * @param id : Retention ID + * @param string $id Retention ID. + * @return string XML file contents. * - * @return XML file in a stream - * - * @throws Facturapi_Exception - **/ - public function download_xml($id) + * @throws FacturapiException + */ + public function downloadXml($id): string { try { - return $this->execute_get_request($this->get_request_url($id) . "/xml"); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to download XML file: ' . $e->getMessage()); + return $this->executeGetRequest($this->getRequestUrl($id) . "/xml"); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } + + /** + * @deprecated Use downloadXml() instead. Will be removed in v5. + */ + public function download_xml($id): string + { + trigger_error('Retentions::download_xml() is deprecated and will be removed in v5. Use downloadXml() instead.', E_USER_DEPRECATED); + return $this->downloadXml($id); + } } diff --git a/src/Resources/Tools.php b/src/Resources/Tools.php index d6a6dbb..36f6a1a 100644 --- a/src/Resources/Tools.php +++ b/src/Resources/Tools.php @@ -3,35 +3,30 @@ namespace Facturapi\Resources; use Facturapi\Http\BaseClient; -use Facturapi\Exceptions\Facturapi_Exception; - -class Tools extends BaseClient { - protected $ENDPOINT = 'tools'; +use Facturapi\Exceptions\FacturapiException; +class Tools extends BaseClient +{ + protected string $ENDPOINT = 'tools'; /** * Validates a tax id * - * @param $params Search parameters - * - * @return JSON validation object + * @param string $tax_id Tax ID (RFC) to validate. + * @return mixed JSON-decoded response. * - * @throws Facturapi_Exception - **/ - public function validateTaxId( $tax_id ) { + * @throws FacturapiException + */ + public function validateTaxId($tax_id): mixed + { try { return json_decode( - $this->execute_get_request( - $this->get_request_url( "tax_id_validation" ).$this->array_to_params( - array( - "tax_id" => $tax_id - ) - ) - ) - ); - } catch ( Facturapi_Exception $e ) { - throw new Facturapi_Exception( 'Failed to validate tax id: ' .$e->getMessage() ); + $this->executeGetRequest( + $this->getRequestUrl('tax_id_validation', array('tax_id' => $tax_id)) + ) + ); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } - -} \ No newline at end of file +} diff --git a/src/Resources/Webhooks.php b/src/Resources/Webhooks.php index 23ced20..2a33210 100644 --- a/src/Resources/Webhooks.php +++ b/src/Resources/Webhooks.php @@ -3,64 +3,60 @@ namespace Facturapi\Resources; use Facturapi\Http\BaseClient; -use Facturapi\Exceptions\Facturapi_Exception; +use Facturapi\Exceptions\FacturapiException; class Webhooks extends BaseClient { - protected $ENDPOINT = 'webhooks'; + protected string $ENDPOINT = 'webhooks'; /** * Get all Webhooks * - * @param Search parameters + * @param array|null $params Search parameters. + * @return mixed JSON-decoded response. * - * @return JSON objects for all Webhooks - * - * @throws Facturapi_Exception - **/ - public function all($params = null) + * @throws FacturapiException + */ + public function all($params = null): mixed { try { - return json_decode($this->execute_get_request($this->get_request_url($params))); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to get webhooks: ' . $e->getMessage()); + return json_decode($this->executeGetRequest($this->getRequestUrl($params))); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** * Get a Webhook by ID * - * @param id : Unique ID for webhook - * - * @return JSON object for requested Webhook + * @param string $id Webhook ID. + * @return mixed JSON-decoded response. * - * @throws Facturapi_Exception - **/ - public function retrieve($id) + * @throws FacturapiException + */ + public function retrieve($id): mixed { try { - return json_decode($this->execute_get_request($this->get_request_url($id))); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to get webhook: ' . $e->getMessage()); + return json_decode($this->executeGetRequest($this->getRequestUrl($id))); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** * Create a Webhook in your organization * - * @param params : array of properties and property values for new webhook - * - * @return Response body with JSON object - * for created Webhook from HTTP POST request + * @param array $params Webhook payload. + * @return mixed JSON-decoded response. * - * @throws Facturapi_Exception - **/ - public function create($params) + * @throws FacturapiException + */ + public function create($params): mixed { try { - return json_decode($this->execute_JSON_post_request($this->get_request_url(), $params)); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to create webhook: ' . $e->getMessage()); + return json_decode($this->executeJsonPostRequest($this->getRequestUrl(), $params)); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } @@ -68,52 +64,51 @@ public function create($params) /** * Update a Webhook in your organization * - * @param $id - * @param $params array of properties and property values for webhook - * - * @return Response body from HTTP POST request - * - * @throws Facturapi_Exception + * @param string $id Webhook ID. + * @param array $params Webhook payload. + * @return mixed JSON-decoded response. * + * @throws FacturapiException */ - public function update($id, $params) + public function update($id, $params): mixed { try { - return json_decode($this->execute_JSON_put_request($this->get_request_url($id), $params)); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to update webhook: ' . $e->getMessage()); + return json_decode($this->executeJsonPutRequest($this->getRequestUrl($id), $params)); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** * Delete a Webhook in your organization * - * @param id : Unique ID for the webhook - * - * @return Response body from HTTP POST request + * @param string $id Webhook ID. + * @return mixed JSON-decoded response. * - * @throws Facturapi_Exception - **/ - public function delete($id) + * @throws FacturapiException + */ + public function delete($id): mixed { try { - return json_decode($this->execute_delete_request($this->get_request_url($id), null)); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception('Unable to delete webhook: ' . $e->getMessage()); + return json_decode($this->executeDeleteRequest($this->getRequestUrl($id), null)); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } /** - * Validate the response of webhook with the secret and facturapi-secret - * @param data: Array of properties according to the signature body [$secret, $facturapi-secret, $payload] - * @return Response Webhook object + * Validates a webhook signature payload. + * + * @param array $data Signature payload. + * @return mixed JSON-decoded response. + * @throws FacturapiException */ - public function validateSignature($data) + public function validateSignature($data): mixed { try { - return json_decode($this->execute_JSON_post_request($this->get_request_url() . '/validate-signature', $data)); - } catch (Facturapi_Exception $e) { - throw new Facturapi_Exception($e); + return json_decode($this->executeJsonPostRequest($this->getRequestUrl() . '/validate-signature', $data)); + } catch (FacturapiException $e) { + throw new FacturapiException($e->getMessage(), 0, $e); } } } From d4a7c151ef784e45af107112faceb46f25fec961 Mon Sep 17 00:00:00 2001 From: javorosas Date: Sun, 29 Mar 2026 23:51:04 +0200 Subject: [PATCH 02/16] docs: rewrite README and refresh v4 release notes --- README.md | 210 ++++++++++++++++++++++++++++-------------------------- VERSION | 21 ++++-- 2 files changed, 124 insertions(+), 107 deletions(-) diff --git a/README.md b/README.md index 82fc6fb..97c96e3 100644 --- a/README.md +++ b/README.md @@ -1,149 +1,153 @@ -Facturapi PHP Library -========= +# Facturapi PHP SDK -This is the official PHP wrapper for https://www.facturapi.io +Official PHP SDK for [Facturapi](https://www.facturapi.io). -FacturAPI makes it easy for developers to generate valid Invoices in Mexico (known as Factura Electrónica or CFDI). - -If you've ever used [Stripe](https://stripe.com) or [Conekta](https://conekta.io), you'll find FacturAPI very straightforward to understand and integrate in your server app. - -## Install +## Installation ```bash -composer require "facturapi/facturapi-php" +composer require facturapi/facturapi-php ``` -## Before you begin +Requirements: +- PHP `>=8.2` -Make sure you have created your free account on [FacturAPI](https://www.facturapi.io) and that you have your **API Keys**. +## Quick Start + +```php +Customers->create([ + 'email' => 'walterwhite@gmail.com', + 'legal_name' => 'Walter White', + 'tax_id' => 'WIWA761018', + 'address' => [ + 'zip' => '06800', + 'street' => 'Av. de los Rosales', + 'exterior' => '123', + 'neighborhood' => 'Tepito', + ], +]); +``` -### Import the library +## Client Configuration -Don't forget to reference the library at the top of your code: +Constructor signature: ```php -use Facturapi\Facturapi; +new Facturapi(string $apiKey, ?array $config = null) ``` -### Create a customer +Supported config keys: +- `apiVersion` (`string`, default: `v2`) +- `timeout` (`int|float`, default: `360` seconds) +- `httpClient` (`Psr\Http\Client\ClientInterface`, advanced) + +Example: ```php +use Facturapi\Facturapi; -// Create an instance of the client. -// You can use different instances for uusing different API Keys -$facturapi = new Facturapi( FACTURAPI_KEY ); - -$customer = array( - "email" => "walterwhite@gmail.com", //Optional but useful to send invoice by email - "legal_name" => "Walter White", // Razón social - "tax_id" => "WIWA761018", //RFC - "address" => array( - "zip"=> "06800", - "street" => "Av. de los Rosales", - "exterior" => "123", - "neighborhood" => "Tepito" - // city, municipality and state are filled automatically from the zip code - // but if you want to, you can override their values - // city: 'México', - // municipality: 'Cuauhtémoc', - // state: 'Ciudad de México' - ) -); - -// Remember to store the customer.id in your records. -// You will need it to create an invoice for this customer. -$new_customer = $facturapi->Customers->create($customer); +$facturapi = new Facturapi($apiKey, [ + 'apiVersion' => 'v2', + 'timeout' => 420, +]); ``` -### Create a product +### Custom HTTP Client (Advanced) + +The SDK works out of the box with its internal Guzzle-based client. + +If you provide `httpClient`, pass any PSR-18 compatible client and configure its timeout values in that client: ```php -$facturapi = new Facturapi( FACTURAPI_KEY ); -$product = array( - "product_key" => "4319150114", // Clave Producto/Servicio from SAT's catalog. Log in to FacturAPI and use our tool to look it up. - "description" => "Apple iPhone 8", - "price" => 345.60 // price in MXN. - // By default, taxes are calculated from the price with IVA 16% - // But again, you can override that by explicitly providing a taxes array - // "taxes" => array( - // array ( "type" => \Facturapi\TaxType::IVA, "rate" => 0.16 ), - // array ( "type" => \Facturapi\TaxType::ISR, "rate" => 0.03666, "withholding" => true ) - // ) -); - -$facturapi->Products->create( $product ); +use Facturapi\Facturapi; +use GuzzleHttp\Client; + +$httpClient = new Client([ + 'timeout' => 420, +]); + +$facturapi = new Facturapi($apiKey, [ + 'httpClient' => $httpClient, +]); ``` -### Create an invoice +## Common Usage + +### Create a Product ```php -$facturapi = new Facturapi( FACTURAPI_KEY ); - -$invoice = array( - "customer" => "YOUR_CUSTOMER_ID", - "items" => array( - array( - "quantity" => 1, // Optional. Defaults to 1. - "product" => "YOUR_PRODUCT_ID" // You can also pass a product object instead - ), - array( - "quantity" => 2, - "product" => array( - "description" => "Guitarra", - "product_key" => "01234567", - "price" => 420.69, - "sku" => "ABC4567" - ) - ) // Add as many products as you want to include in your invoice - ), - "payment_form" => \Facturapi\PaymentForm::EFECTIVO, - "folio_number" => "581", - "series" => "F" -); - -$facturapi->Invoices->create( $invoice ); +$product = $facturapi->Products->create([ + 'product_key' => '4319150114', + 'description' => 'Apple iPhone 8', + 'price' => 345.60, +]); ``` -#### Download your invoice +### Create an Invoice ```php -// Once you have successfully created your invoice, you can... -$facturapi = new Facturapi( FACTURAPI_KEY ); - -$facturapi->Invoices->download_zip("INVOICE_ID") // stream containing the PDF and XML as a ZIP file or +$invoice = $facturapi->Invoices->create([ + 'customer' => 'YOUR_CUSTOMER_ID', + 'items' => [[ + 'quantity' => 1, + 'product' => 'YOUR_PRODUCT_ID', + ]], + 'payment_form' => \Facturapi\PaymentForm::EFECTIVO, + 'folio_number' => '581', + 'series' => 'F', +]); +``` -$facturapi->Invoices->download_pdf("INVOICE_ID") // stream containing the PDF file or +### Download Files -$facturapi->Invoices->download_xml("INVOICE_ID") // stream containing the XML file or +```php +$zipBytes = $facturapi->Invoices->downloadZip('INVOICE_ID'); +$pdfBytes = $facturapi->Invoices->downloadPdf('INVOICE_ID'); +$xmlBytes = $facturapi->Invoices->downloadXml('INVOICE_ID'); ``` -#### Send your invoice by email +`downloadPdf()` returns raw PDF bytes (binary string), not base64. ```php -// Send the invoice to your customer's email (if any) -$facturapi = new Facturapi( FACTURAPI_KEY ); - -$facturapi->Invoices->send_by_email("INVOICE_ID"); +file_put_contents('invoice.pdf', $pdfBytes); ``` -## Documentation +### Send by Email -There's more you can do with this library: List, retrieve, update, and remove Customers, Products and Invoices. +```php +$facturapi->Invoices->sendByEmail('INVOICE_ID'); +``` -Visit the full documentation at http://docs.facturapi.io. +### Comercio Exterior Catalogs -## Help +```php +$results = $facturapi->ComercioExteriorCatalogs->searchTariffFractions([ + 'q' => '0101', + 'page' => 0, + 'limit' => 10, +]); +``` -### Found a bug? +## Migration Notes (v4) -Please report it on the Issue Tracker +- Minimum PHP version is now `>=8.2`. +- Snake_case method aliases are deprecated in v4 and will be removed in v5. +- `Facturapi\\Exceptions\\Facturapi_Exception` is deprecated in v4 and will be removed in v5. +- Use `Facturapi\\Exceptions\\FacturapiException`. -### Want to contribute? +## Documentation -Send us your PR! We appreciate your help :) +Full docs: [https://docs.facturapi.io](https://docs.facturapi.io) -### Contact us! +## Support -contacto@facturapi.io \ No newline at end of file +- Issues: open a GitHub issue +- Email: `contacto@facturapi.io` diff --git a/VERSION b/VERSION index c8c4a70..ac9743a 100644 --- a/VERSION +++ b/VERSION @@ -1,5 +1,18 @@ -3.7.0 +4.0.0 -## What changed -- Added `ComercioExteriorCatalogs::searchTariffFractions` support for Comercio Exterior catalog queries. -- Added deploy automation from `main` using semver + release notes defined in `VERSION`. +## Breaking +- Bumped minimum supported PHP version to `>=8.2`. +- Simplified SDK constructor to a single optional `config` parameter: + - `apiVersion` (defaults to `v2`) + - `timeout` (seconds) + - `httpClient` (PSR-18 client, advanced) +- Removed support for the positional `apiVersion` constructor argument. + +## Improvements +- Added camelCase method names for invoice/receipt/retention actions. +- Kept snake_case aliases for transition compatibility. +- Enabled strict TLS verification by default for HTTP requests. +- Added `ComercioExteriorCatalogs` to the main `Facturapi` client. +- Standardized constructor argument names to camelCase. +- Standardized internal protected method names to camelCase. +- Renamed exception class to `FacturapiException` and kept `Facturapi_Exception` as a compatibility alias. From 0ce8651f437befae676e0407e51aa791664a20cb Mon Sep 17 00:00:00 2001 From: javorosas Date: Sun, 29 Mar 2026 23:53:31 +0200 Subject: [PATCH 03/16] docs: add modern packagist badges to README --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 97c96e3..700c7a2 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,12 @@ Official PHP SDK for [Facturapi](https://www.facturapi.io). +[![Latest Version](https://img.shields.io/packagist/v/facturapi/facturapi-php?style=flat-square)](https://packagist.org/packages/facturapi/facturapi-php) +[![PHP Version](https://img.shields.io/packagist/php-v/facturapi/facturapi-php?style=flat-square)](https://packagist.org/packages/facturapi/facturapi-php) +[![Total Downloads](https://img.shields.io/packagist/dt/facturapi/facturapi-php?style=flat-square)](https://packagist.org/packages/facturapi/facturapi-php) +[![Monthly Downloads](https://img.shields.io/packagist/dm/facturapi/facturapi-php?style=flat-square)](https://packagist.org/packages/facturapi/facturapi-php) +[![License](https://img.shields.io/packagist/l/facturapi/facturapi-php?style=flat-square)](https://packagist.org/packages/facturapi/facturapi-php) + ## Installation ```bash From 1337cde2e6fb6817cfa3da07caae2d92fa829424 Mon Sep 17 00:00:00 2001 From: javorosas Date: Sun, 29 Mar 2026 23:54:04 +0200 Subject: [PATCH 04/16] docs: add light emoji accents to README sections --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 700c7a2..b8ad84e 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Official PHP SDK for [Facturapi](https://www.facturapi.io). [![Monthly Downloads](https://img.shields.io/packagist/dm/facturapi/facturapi-php?style=flat-square)](https://packagist.org/packages/facturapi/facturapi-php) [![License](https://img.shields.io/packagist/l/facturapi/facturapi-php?style=flat-square)](https://packagist.org/packages/facturapi/facturapi-php) -## Installation +## Installation ⚡ ```bash composer require facturapi/facturapi-php @@ -17,7 +17,7 @@ composer require facturapi/facturapi-php Requirements: - PHP `>=8.2` -## Quick Start +## Quick Start 🚀 ```php Customers->create([ ]); ``` -## Client Configuration +## Client Configuration ⚙️ Constructor signature: @@ -85,7 +85,7 @@ $facturapi = new Facturapi($apiKey, [ ]); ``` -## Common Usage +## Common Usage 🧾 ### Create a Product @@ -142,18 +142,18 @@ $results = $facturapi->ComercioExteriorCatalogs->searchTariffFractions([ ]); ``` -## Migration Notes (v4) +## Migration Notes (v4) 🔄 - Minimum PHP version is now `>=8.2`. - Snake_case method aliases are deprecated in v4 and will be removed in v5. - `Facturapi\\Exceptions\\Facturapi_Exception` is deprecated in v4 and will be removed in v5. - Use `Facturapi\\Exceptions\\FacturapiException`. -## Documentation +## Documentation 📚 Full docs: [https://docs.facturapi.io](https://docs.facturapi.io) -## Support +## Support 💬 - Issues: open a GitHub issue - Email: `contacto@facturapi.io` From 47e2c71d68b0647c0cbfb5a7f120fd63416c3946 Mon Sep 17 00:00:00 2001 From: javorosas Date: Sun, 29 Mar 2026 23:56:15 +0200 Subject: [PATCH 05/16] docs: add Spanish README and language switch links --- README.es.md | 161 +++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 2 + 2 files changed, 163 insertions(+) create mode 100644 README.es.md diff --git a/README.es.md b/README.es.md new file mode 100644 index 0000000..636f436 --- /dev/null +++ b/README.es.md @@ -0,0 +1,161 @@ +# Facturapi PHP SDK + +SDK oficial de PHP para [Facturapi](https://www.facturapi.io). + +Idioma: Español | [English](./README.md) + +[![Última Versión](https://img.shields.io/packagist/v/facturapi/facturapi-php?style=flat-square)](https://packagist.org/packages/facturapi/facturapi-php) +[![Versión de PHP](https://img.shields.io/packagist/php-v/facturapi/facturapi-php?style=flat-square)](https://packagist.org/packages/facturapi/facturapi-php) +[![Descargas Totales](https://img.shields.io/packagist/dt/facturapi/facturapi-php?style=flat-square)](https://packagist.org/packages/facturapi/facturapi-php) +[![Descargas Mensuales](https://img.shields.io/packagist/dm/facturapi/facturapi-php?style=flat-square)](https://packagist.org/packages/facturapi/facturapi-php) +[![Licencia](https://img.shields.io/packagist/l/facturapi/facturapi-php?style=flat-square)](https://packagist.org/packages/facturapi/facturapi-php) + +## Instalación ⚡ + +```bash +composer require facturapi/facturapi-php +``` + +Requisitos: +- PHP `>=8.2` + +## Inicio Rápido 🚀 + +```php +Customers->create([ + 'email' => 'walterwhite@gmail.com', + 'legal_name' => 'Walter White', + 'tax_id' => 'WIWA761018', + 'address' => [ + 'zip' => '06800', + 'street' => 'Av. de los Rosales', + 'exterior' => '123', + 'neighborhood' => 'Tepito', + ], +]); +``` + +## Configuración del Cliente ⚙️ + +Firma del constructor: + +```php +new Facturapi(string $apiKey, ?array $config = null) +``` + +Claves soportadas en `config`: +- `apiVersion` (`string`, valor por defecto: `v2`) +- `timeout` (`int|float`, valor por defecto: `360` segundos) +- `httpClient` (`Psr\Http\Client\ClientInterface`, avanzado) + +Ejemplo: + +```php +use Facturapi\Facturapi; + +$facturapi = new Facturapi($apiKey, [ + 'apiVersion' => 'v2', + 'timeout' => 420, +]); +``` + +### Cliente HTTP Personalizado (Avanzado) + +El SDK funciona sin configuración adicional con su cliente interno basado en Guzzle. + +Si proporcionas `httpClient`, puedes pasar cualquier cliente compatible con PSR-18 y configurar ahí mismo los timeouts: + +```php +use Facturapi\Facturapi; +use GuzzleHttp\Client; + +$httpClient = new Client([ + 'timeout' => 420, +]); + +$facturapi = new Facturapi($apiKey, [ + 'httpClient' => $httpClient, +]); +``` + +## Uso Común 🧾 + +### Crear un Producto + +```php +$product = $facturapi->Products->create([ + 'product_key' => '4319150114', + 'description' => 'Apple iPhone 8', + 'price' => 345.60, +]); +``` + +### Crear una Factura + +```php +$invoice = $facturapi->Invoices->create([ + 'customer' => 'YOUR_CUSTOMER_ID', + 'items' => [[ + 'quantity' => 1, + 'product' => 'YOUR_PRODUCT_ID', + ]], + 'payment_form' => \Facturapi\PaymentForm::EFECTIVO, + 'folio_number' => '581', + 'series' => 'F', +]); +``` + +### Descargar Archivos + +```php +$zipBytes = $facturapi->Invoices->downloadZip('INVOICE_ID'); +$pdfBytes = $facturapi->Invoices->downloadPdf('INVOICE_ID'); +$xmlBytes = $facturapi->Invoices->downloadXml('INVOICE_ID'); +``` + +`downloadPdf()` devuelve bytes crudos de PDF (cadena binaria), no base64. + +```php +file_put_contents('invoice.pdf', $pdfBytes); +``` + +### Enviar por Correo + +```php +$facturapi->Invoices->sendByEmail('INVOICE_ID'); +``` + +### Catálogos de Comercio Exterior + +```php +$results = $facturapi->ComercioExteriorCatalogs->searchTariffFractions([ + 'q' => '0101', + 'page' => 0, + 'limit' => 10, +]); +``` + +## Notas de Migración (v4) 🔄 + +- La versión mínima de PHP ahora es `>=8.2`. +- Los aliases snake_case están deprecados en v4 y se eliminarán en v5. +- `Facturapi\\Exceptions\\Facturapi_Exception` está deprecada en v4 y se eliminará en v5. +- Usa `Facturapi\\Exceptions\\FacturapiException`. + +## Documentación 📚 + +Documentación completa: [https://docs.facturapi.io](https://docs.facturapi.io) + +## Soporte 💬 + +- Issues: abre un issue en GitHub +- Email: `contacto@facturapi.io` diff --git a/README.md b/README.md index b8ad84e..2f915dc 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Official PHP SDK for [Facturapi](https://www.facturapi.io). +Language: English | [Español](./README.es.md) + [![Latest Version](https://img.shields.io/packagist/v/facturapi/facturapi-php?style=flat-square)](https://packagist.org/packages/facturapi/facturapi-php) [![PHP Version](https://img.shields.io/packagist/php-v/facturapi/facturapi-php?style=flat-square)](https://packagist.org/packages/facturapi/facturapi-php) [![Total Downloads](https://img.shields.io/packagist/dt/facturapi/facturapi-php?style=flat-square)](https://packagist.org/packages/facturapi/facturapi-php) From 415d5d981f7d55e96d1624a26f98737e7cd2ae92 Mon Sep 17 00:00:00 2001 From: javorosas Date: Mon, 30 Mar 2026 12:53:34 +0200 Subject: [PATCH 06/16] test: add phpunit coverage for auth, downloads, multipart and webhooks --- phpunit.xml | 12 ++++ src/Resources/Webhooks.php | 24 +++++++ tests/Resources/InvoicesTest.php | 29 ++++++++ .../Resources/OrganizationsMultipartTest.php | 72 +++++++++++++++++++ tests/Resources/WebhooksTest.php | 45 ++++++++++++ tests/Support/FakeHttpClient.php | 46 ++++++++++++ tests/bootstrap.php | 9 +++ 7 files changed, 237 insertions(+) create mode 100644 phpunit.xml create mode 100644 tests/Resources/InvoicesTest.php create mode 100644 tests/Resources/OrganizationsMultipartTest.php create mode 100644 tests/Resources/WebhooksTest.php create mode 100644 tests/Support/FakeHttpClient.php create mode 100644 tests/bootstrap.php diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..7a56fa3 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,12 @@ + + + + + tests + + + diff --git a/src/Resources/Webhooks.php b/src/Resources/Webhooks.php index 2a33210..b9add82 100644 --- a/src/Resources/Webhooks.php +++ b/src/Resources/Webhooks.php @@ -111,4 +111,28 @@ public function validateSignature($data): mixed throw new FacturapiException($e->getMessage(), 0, $e); } } + + /** + * Verifies a webhook signature locally using HMAC-SHA256. + * + * Accepted signature formats: + * - raw hex digest + * - prefixed hex digest (e.g. "sha256=") + * + * @param string $rawBody Raw webhook request body. + * @param string $signature Signature from webhook headers. + * @param string $webhookSecret Webhook signing secret. + * @return bool True when signature is valid. + */ + public static function verifySignatureLocally(string $rawBody, string $signature, string $webhookSecret): bool + { + $normalizedSignature = trim($signature); + if (str_starts_with($normalizedSignature, 'sha256=')) { + $normalizedSignature = substr($normalizedSignature, 7); + } + + $expectedHex = hash_hmac('sha256', $rawBody, $webhookSecret); + + return hash_equals($expectedHex, $normalizedSignature); + } } diff --git a/tests/Resources/InvoicesTest.php b/tests/Resources/InvoicesTest.php new file mode 100644 index 0000000..087a5b5 --- /dev/null +++ b/tests/Resources/InvoicesTest.php @@ -0,0 +1,29 @@ + $httpClient]); + + $result = $invoices->downloadPdf('inv_123'); + + self::assertSame('PDF_BINARY_CONTENT', $result); + + $request = $httpClient->requests()[0]; + self::assertSame('GET', $request->getMethod()); + self::assertSame('https://www.facturapi.io/v2/invoices/inv_123/pdf', (string) $request->getUri()); + self::assertSame('Basic ' . base64_encode('sk_test_abc123:'), $request->getHeaderLine('Authorization')); + self::assertSame('facturapi-php', $request->getHeaderLine('User-Agent')); + } +} diff --git a/tests/Resources/OrganizationsMultipartTest.php b/tests/Resources/OrganizationsMultipartTest.php new file mode 100644 index 0000000..a63ff2b --- /dev/null +++ b/tests/Resources/OrganizationsMultipartTest.php @@ -0,0 +1,72 @@ + $httpClient]); + + $result = $organizations->uploadLogo('org_123', $tmpLogo); + + self::assertTrue($result->ok); + + $request = $httpClient->requests()[0]; + self::assertSame('PUT', $request->getMethod()); + self::assertSame('https://www.facturapi.io/v2/organizations/org_123/logo', (string) $request->getUri()); + self::assertStringStartsWith('multipart/form-data; boundary=', $request->getHeaderLine('Content-Type')); + + $body = (string) $request->getBody(); + self::assertStringContainsString('name="file"', $body); + self::assertStringContainsString('filename="' . basename($tmpLogo) . '"', $body); + self::assertStringContainsString('LOGO_BYTES', $body); + + @unlink($tmpLogo); + } + + public function testUploadCertificateBuildsMultipartRequestWithCerKeyAndPassword(): void + { + $tmpCer = tempnam(sys_get_temp_dir(), 'cer_'); + $tmpKey = tempnam(sys_get_temp_dir(), 'key_'); + file_put_contents($tmpCer, 'CER_BYTES'); + file_put_contents($tmpKey, 'KEY_BYTES'); + + $httpClient = new FakeHttpClient(new Response(200, [], '{"ok":true}')); + $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + + $result = $organizations->uploadCertificate('org_123', [ + 'cerFile' => $tmpCer, + 'keyFile' => $tmpKey, + 'password' => 'secret_password', + ]); + + self::assertTrue($result->ok); + + $request = $httpClient->requests()[0]; + self::assertSame('PUT', $request->getMethod()); + self::assertSame('https://www.facturapi.io/v2/organizations/org_123/certificate', (string) $request->getUri()); + + $body = (string) $request->getBody(); + self::assertStringContainsString('name="cer"', $body); + self::assertStringContainsString('name="key"', $body); + self::assertStringContainsString('name="password"', $body); + self::assertStringContainsString('CER_BYTES', $body); + self::assertStringContainsString('KEY_BYTES', $body); + self::assertStringContainsString('secret_password', $body); + + @unlink($tmpCer); + @unlink($tmpKey); + } +} diff --git a/tests/Resources/WebhooksTest.php b/tests/Resources/WebhooksTest.php new file mode 100644 index 0000000..2d520b0 --- /dev/null +++ b/tests/Resources/WebhooksTest.php @@ -0,0 +1,45 @@ + $httpClient]); + + $payload = [ + 'body' => '{"id":"evt_123"}', + 'signature' => 'sha256=fake_signature', + ]; + + $result = $webhooks->validateSignature($payload); + + self::assertTrue($result->valid); + + $request = $httpClient->requests()[0]; + self::assertSame('POST', $request->getMethod()); + self::assertSame('https://www.facturapi.io/v2/webhooks/validate-signature', (string) $request->getUri()); + self::assertSame('application/json', $request->getHeaderLine('Content-Type')); + self::assertSame(json_encode($payload), (string) $request->getBody()); + } + + public function testVerifySignatureLocallyAcceptsHexAndPrefixedSha256Formats(): void + { + $rawPayload = '{"id":"evt_123"}'; + $secret = 'whsec_test_secret'; + $hex = hash_hmac('sha256', $rawPayload, $secret); + + self::assertTrue(Webhooks::verifySignatureLocally($rawPayload, $hex, $secret)); + self::assertTrue(Webhooks::verifySignatureLocally($rawPayload, 'sha256=' . $hex, $secret)); + self::assertFalse(Webhooks::verifySignatureLocally($rawPayload, 'sha256=invalid', $secret)); + } +} diff --git a/tests/Support/FakeHttpClient.php b/tests/Support/FakeHttpClient.php new file mode 100644 index 0000000..f13a2c2 --- /dev/null +++ b/tests/Support/FakeHttpClient.php @@ -0,0 +1,46 @@ + */ + private array $requests = []; + + /** @var list */ + private array $responses; + + public function __construct(ResponseInterface ...$responses) + { + $this->responses = $responses; + } + + public function sendRequest(RequestInterface $request): ResponseInterface + { + $this->requests[] = $request; + + if ($this->responses === []) { + throw new FakeHttpClientException('No fake response queued.'); + } + + return array_shift($this->responses); + } + + /** @return list */ + public function requests(): array + { + return $this->requests; + } +} + +final class FakeHttpClientException extends RuntimeException implements ClientExceptionInterface +{ +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..ceb9593 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,9 @@ + Date: Mon, 30 Mar 2026 12:56:41 +0200 Subject: [PATCH 07/16] refactor(webhooks): validate signature locally first, fallback to API --- src/Resources/Webhooks.php | 45 +++++++++++++++++++++++-------- tests/Resources/WebhooksTest.php | 46 +++++++++++++++++++++++++++----- 2 files changed, 74 insertions(+), 17 deletions(-) diff --git a/src/Resources/Webhooks.php b/src/Resources/Webhooks.php index b9add82..c7022ec 100644 --- a/src/Resources/Webhooks.php +++ b/src/Resources/Webhooks.php @@ -99,12 +99,27 @@ public function delete($id): mixed /** * Validates a webhook signature payload. * + * Local verification is attempted first when `body`, `signature`, and + * `webhookSecret` are provided in `$data`. If local verification cannot be + * performed for any reason, the SDK falls back to the API endpoint. + * * @param array $data Signature payload. * @return mixed JSON-decoded response. * @throws FacturapiException */ public function validateSignature($data): mixed { + if (is_array($data)) { + try { + $localResult = $this->verifySignatureLocallyFromPayload($data); + if ($localResult !== null) { + return (object) array('valid' => $localResult); + } + } catch (\Throwable $ignored) { + // Fallback to server-side verification. + } + } + try { return json_decode($this->executeJsonPostRequest($this->getRequestUrl() . '/validate-signature', $data)); } catch (FacturapiException $e) { @@ -113,18 +128,26 @@ public function validateSignature($data): mixed } /** - * Verifies a webhook signature locally using HMAC-SHA256. - * - * Accepted signature formats: - * - raw hex digest - * - prefixed hex digest (e.g. "sha256=") - * - * @param string $rawBody Raw webhook request body. - * @param string $signature Signature from webhook headers. - * @param string $webhookSecret Webhook signing secret. - * @return bool True when signature is valid. + * Attempts local signature verification when payload has required fields. + * Returns null when local verification cannot be attempted. + */ + private function verifySignatureLocallyFromPayload(array $data): ?bool + { + $rawBody = $data['body'] ?? $data['payload'] ?? $data['rawBody'] ?? null; + $signature = $data['signature'] ?? $data['x-signature'] ?? $data['x_signature'] ?? null; + $webhookSecret = $data['webhookSecret'] ?? $data['webhook_secret'] ?? $data['secret'] ?? null; + + if (!is_string($rawBody) || !is_string($signature) || !is_string($webhookSecret)) { + return null; + } + + return $this->verifySignatureLocally($rawBody, $signature, $webhookSecret); + } + + /** + * Local HMAC-SHA256 signature verification. */ - public static function verifySignatureLocally(string $rawBody, string $signature, string $webhookSecret): bool + private function verifySignatureLocally(string $rawBody, string $signature, string $webhookSecret): bool { $normalizedSignature = trim($signature); if (str_starts_with($normalizedSignature, 'sha256=')) { diff --git a/tests/Resources/WebhooksTest.php b/tests/Resources/WebhooksTest.php index 2d520b0..2d16234 100644 --- a/tests/Resources/WebhooksTest.php +++ b/tests/Resources/WebhooksTest.php @@ -11,7 +11,26 @@ final class WebhooksTest extends TestCase { - public function testValidateSignatureCallsApiEndpointWithJsonPayload(): void + public function testValidateSignatureUsesLocalVerificationByDefault(): void + { + $httpClient = new FakeHttpClient(); + $webhooks = new Webhooks('sk_test_abc123', ['httpClient' => $httpClient]); + + $rawPayload = '{"id":"evt_123"}'; + $secret = 'whsec_test_secret'; + $hex = hash_hmac('sha256', $rawPayload, $secret); + + $result = $webhooks->validateSignature([ + 'body' => $rawPayload, + 'signature' => 'sha256=' . $hex, + 'webhookSecret' => $secret, + ]); + + self::assertTrue($result->valid); + self::assertCount(0, $httpClient->requests()); + } + + public function testValidateSignatureFallsBackToApiEndpointWhenLocalCannotRun(): void { $httpClient = new FakeHttpClient(new Response(200, [], '{"valid":true}')); $webhooks = new Webhooks('sk_test_abc123', ['httpClient' => $httpClient]); @@ -19,6 +38,7 @@ public function testValidateSignatureCallsApiEndpointWithJsonPayload(): void $payload = [ 'body' => '{"id":"evt_123"}', 'signature' => 'sha256=fake_signature', + // No webhookSecret -> triggers API fallback ]; $result = $webhooks->validateSignature($payload); @@ -32,14 +52,28 @@ public function testValidateSignatureCallsApiEndpointWithJsonPayload(): void self::assertSame(json_encode($payload), (string) $request->getBody()); } - public function testVerifySignatureLocallyAcceptsHexAndPrefixedSha256Formats(): void + public function testValidateSignatureLocalSupportsRawHexAndRejectsInvalidSignatures(): void { - $rawPayload = '{"id":"evt_123"}'; + $httpClient = new FakeHttpClient(); + $webhooks = new Webhooks('sk_test_abc123', ['httpClient' => $httpClient]); + + $rawPayload = '{"id":"evt_456"}'; $secret = 'whsec_test_secret'; $hex = hash_hmac('sha256', $rawPayload, $secret); - self::assertTrue(Webhooks::verifySignatureLocally($rawPayload, $hex, $secret)); - self::assertTrue(Webhooks::verifySignatureLocally($rawPayload, 'sha256=' . $hex, $secret)); - self::assertFalse(Webhooks::verifySignatureLocally($rawPayload, 'sha256=invalid', $secret)); + $valid = $webhooks->validateSignature([ + 'payload' => $rawPayload, + 'signature' => $hex, + 'secret' => $secret, + ]); + $invalid = $webhooks->validateSignature([ + 'payload' => $rawPayload, + 'signature' => 'sha256=invalid', + 'secret' => $secret, + ]); + + self::assertTrue($valid->valid); + self::assertFalse($invalid->valid); + self::assertCount(0, $httpClient->requests()); } } From 910647065036f926c4aacc49163e887e0c1b805a Mon Sep 17 00:00:00 2001 From: javorosas Date: Mon, 30 Mar 2026 13:09:53 +0200 Subject: [PATCH 08/16] feat(v4): preserve API error payloads and expand critical tests --- README.es.md | 21 ++++++ README.md | 21 ++++++ VERSION | 10 +++ src/Exceptions/FacturapiException.php | 42 ++++++++++- src/Http/BaseClient.php | 54 ++++++++++++- src/Resources/CartaPorteCatalogs.php | 20 ++--- src/Resources/Catalogs.php | 4 +- src/Resources/ComercioExteriorCatalogs.php | 2 +- src/Resources/Customers.php | 12 +-- src/Resources/Invoices.php | 30 ++++---- src/Resources/Organizations.php | 44 +++++------ src/Resources/Products.php | 10 +-- src/Resources/Receipts.php | 16 ++-- src/Resources/Retentions.php | 16 ++-- src/Resources/Tools.php | 2 +- src/Resources/Webhooks.php | 12 +-- .../ErrorHandlingNonJsonEdgeCasesTest.php | 75 +++++++++++++++++++ tests/Http/ErrorHandlingTest.php | 71 ++++++++++++++++++ .../Resources/InvoiceReceiptCreationTest.php | 70 +++++++++++++++++ .../Resources/OrganizationsMultipartTest.php | 25 +++++++ 20 files changed, 471 insertions(+), 86 deletions(-) create mode 100644 tests/Http/ErrorHandlingNonJsonEdgeCasesTest.php create mode 100644 tests/Http/ErrorHandlingTest.php create mode 100644 tests/Resources/InvoiceReceiptCreationTest.php diff --git a/README.es.md b/README.es.md index 636f436..921ac46 100644 --- a/README.es.md +++ b/README.es.md @@ -144,6 +144,27 @@ $results = $facturapi->ComercioExteriorCatalogs->searchTariffFractions([ ]); ``` +## Manejo de Errores ⚠️ + +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. +- `getErrorData()`: payload JSON decodificado del error (shape completo del API). +- `getRawBody()`: cuerpo crudo de la respuesta. + +```php +use Facturapi\Exceptions\FacturapiException; + +try { + $facturapi->Invoices->create($payload); +} catch (FacturapiException $e) { + $status = $e->getStatusCode(); + $error = $e->getErrorData(); // Incluye campos como code, message, path/details cuando el API los retorna. +} +``` + ## Notas de Migración (v4) 🔄 - La versión mínima de PHP ahora es `>=8.2`. diff --git a/README.md b/README.md index 2f915dc..7d57ca1 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,27 @@ $results = $facturapi->ComercioExteriorCatalogs->searchTariffFractions([ ]); ``` +## Error Handling ⚠️ + +On non-2xx responses, the SDK throws `Facturapi\Exceptions\FacturapiException`. + +The exception includes: +- `getMessage()`: API error message when present. +- `getStatusCode()`: HTTP status code. +- `getErrorData()`: decoded JSON error payload (full API shape). +- `getRawBody()`: raw response body string. + +```php +use Facturapi\Exceptions\FacturapiException; + +try { + $facturapi->Invoices->create($payload); +} catch (FacturapiException $e) { + $status = $e->getStatusCode(); + $error = $e->getErrorData(); // Includes fields like code, message, path/details when returned by API. +} +``` + ## Migration Notes (v4) 🔄 - Minimum PHP version is now `>=8.2`. diff --git a/VERSION b/VERSION index ac9743a..b39d38f 100644 --- a/VERSION +++ b/VERSION @@ -8,10 +8,20 @@ - `httpClient` (PSR-18 client, advanced) - Removed support for the positional `apiVersion` constructor argument. +## Safe Update (No Migration Needed) +- You can update without code changes if: + - Your project already runs on PHP `>=8.2`. + - You instantiate the SDK as `new Facturapi($apiKey)` (single API key argument). + +## Deprecations (Non-Breaking) +- Snake_case aliases remain functional in v4, but are deprecated and will be removed in v5. +- `Facturapi_Exception` remains functional in v4 through a compatibility alias, but is deprecated and will be removed in v5. + ## Improvements - Added camelCase method names for invoice/receipt/retention actions. - Kept snake_case aliases for transition compatibility. - Enabled strict TLS verification by default for HTTP requests. +- `Webhooks::validateSignature()` now verifies locally by default when payload includes `body`/`payload`, `signature`, and `webhookSecret`, with automatic fallback to API validation. - Added `ComercioExteriorCatalogs` to the main `Facturapi` client. - Standardized constructor argument names to camelCase. - Standardized internal protected method names to camelCase. diff --git a/src/Exceptions/FacturapiException.php b/src/Exceptions/FacturapiException.php index 43deadb..5ad4f83 100644 --- a/src/Exceptions/FacturapiException.php +++ b/src/Exceptions/FacturapiException.php @@ -3,5 +3,45 @@ namespace Facturapi\Exceptions; use Exception; +use Throwable; -class FacturapiException extends Exception {} +class FacturapiException extends Exception +{ + private mixed $errorData; + private ?int $statusCode; + private ?string $rawBody; + + public function __construct( + string $message = '', + int $code = 0, + ?Throwable $previous = null, + mixed $errorData = null, + ?int $statusCode = null, + ?string $rawBody = null + ) { + parent::__construct($message, $code, $previous); + $this->errorData = $errorData; + $this->statusCode = $statusCode; + $this->rawBody = $rawBody; + } + + public function getErrorData(): mixed + { + return $this->errorData; + } + + public function getResponseData(): mixed + { + return $this->errorData; + } + + public function getStatusCode(): ?int + { + return $this->statusCode; + } + + public function getRawBody(): ?string + { + return $this->rawBody; + } +} diff --git a/src/Http/BaseClient.php b/src/Http/BaseClient.php index 09f6c41..74219b3 100644 --- a/src/Http/BaseClient.php +++ b/src/Http/BaseClient.php @@ -345,12 +345,64 @@ protected function executeRequest($method, $url, $headers = array(), $body = nul $output = (string) $response->getBody(); if ($validateStatus && ($this->lastStatus < 200 || $this->lastStatus > 299)) { - throw new FacturapiException($output); + $decoded = json_decode($output, true); + $errorData = is_array($decoded) ? $decoded : null; + $message = $this->extractErrorMessage($errorData, $output); + + throw new FacturapiException( + $message, + 0, + null, + $errorData, + $this->lastStatus, + $output + ); } return $output; } + /** + * Extracts a human-readable message from known API error shapes. + * + * @param array|null $errorData Decoded API error payload. + * @param string $rawBody Raw response body. + * @return string + */ + private function extractErrorMessage(?array $errorData, string $rawBody): string + { + if ($errorData === null) { + return $rawBody; + } + + if (isset($errorData['message']) && is_string($errorData['message'])) { + return $errorData['message']; + } + + if ( + isset($errorData['error']) && + is_array($errorData['error']) && + isset($errorData['error']['message']) && + is_string($errorData['error']['message']) + ) { + return $errorData['error']['message']; + } + + if ( + isset($errorData['errors']) && + is_array($errorData['errors']) && + isset($errorData['errors'][0]) && + is_array($errorData['errors'][0]) && + isset($errorData['errors'][0]['message']) && + is_string($errorData['errors'][0]['message']) + ) { + return $errorData['errors'][0]['message']; + } + + $encoded = json_encode($errorData); + return $encoded === false ? $rawBody : $encoded; + } + /** * Builds base headers for API requests. * diff --git a/src/Resources/CartaPorteCatalogs.php b/src/Resources/CartaPorteCatalogs.php index 9de54e0..1fe95d4 100644 --- a/src/Resources/CartaPorteCatalogs.php +++ b/src/Resources/CartaPorteCatalogs.php @@ -25,7 +25,7 @@ public function searchAirTransportCodes($params = null): mixed ) ); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -45,7 +45,7 @@ public function searchTransportConfigs($params = null): mixed ) ); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -65,7 +65,7 @@ public function searchRightsOfPassage($params = null): mixed ) ); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -85,7 +85,7 @@ public function searchCustomsDocuments($params = null): mixed ) ); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -105,7 +105,7 @@ public function searchPackagingTypes($params = null): mixed ) ); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -125,7 +125,7 @@ public function searchTrailerTypes($params = null): mixed ) ); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -145,7 +145,7 @@ public function searchHazardousMaterials($params = null): mixed ) ); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -165,7 +165,7 @@ public function searchNavalAuthorizations($params = null): mixed ) ); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -185,7 +185,7 @@ public function searchPortStations($params = null): mixed ) ); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -205,7 +205,7 @@ public function searchMarineContainers($params = null): mixed ) ); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } } diff --git a/src/Resources/Catalogs.php b/src/Resources/Catalogs.php index 434f4ed..b74eacb 100644 --- a/src/Resources/Catalogs.php +++ b/src/Resources/Catalogs.php @@ -26,7 +26,7 @@ public function searchProducts($params = null): mixed ) ); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -47,7 +47,7 @@ public function searchUnits($params = null): mixed ) ); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } } diff --git a/src/Resources/ComercioExteriorCatalogs.php b/src/Resources/ComercioExteriorCatalogs.php index 2adef5e..2016eea 100644 --- a/src/Resources/ComercioExteriorCatalogs.php +++ b/src/Resources/ComercioExteriorCatalogs.php @@ -25,7 +25,7 @@ public function searchTariffFractions($params = null): mixed ) ); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } } diff --git a/src/Resources/Customers.php b/src/Resources/Customers.php index a3a7a9f..c4ed04a 100644 --- a/src/Resources/Customers.php +++ b/src/Resources/Customers.php @@ -20,7 +20,7 @@ public function all( $params = null ): mixed { try { return json_decode( $this->executeGetRequest( $this->getRequestUrl( $params ) ) ); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -36,7 +36,7 @@ public function retrieve( $id ): mixed { try { return json_decode( $this->executeGetRequest( $this->getRequestUrl( $id ) ) ); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -53,7 +53,7 @@ public function create( $data, $params = null ): mixed { try { return json_decode( $this->executeJsonPostRequest( $this->getRequestUrl($params), $data ) ); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -72,7 +72,7 @@ public function update( $id, $data, $params = null ): mixed { try { return json_decode( $this->executeJsonPutRequest( $this->getRequestUrl( $id, $params ), $data ) ); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -88,7 +88,7 @@ public function delete( $id ): mixed { try { return json_decode( $this->executeDeleteRequest( $this->getRequestUrl( $id ), null ) ); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -104,7 +104,7 @@ public function validateTaxInfo( $id ): mixed { try { return json_decode( $this->executeGetRequest( $this->getRequestUrl( $id ) . "/tax-info-validation" ) ); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } diff --git a/src/Resources/Invoices.php b/src/Resources/Invoices.php index 1249979..21fbf06 100644 --- a/src/Resources/Invoices.php +++ b/src/Resources/Invoices.php @@ -20,7 +20,7 @@ public function all( $query = null ): mixed { try { return json_decode( $this->executeGetRequest( $this->getRequestUrl( $query ) ) ); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -35,7 +35,7 @@ public function retrieve( $id ): mixed { try { return json_decode( $this->executeGetRequest( $this->getRequestUrl( $id ) ) ); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -51,7 +51,7 @@ public function create( $body, $query = null): mixed { try { return json_decode( $this->executeJsonPostRequest( $this->getRequestUrl($query), $body) ); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -68,7 +68,7 @@ public function cancel( $id, $query ): mixed { try { return json_decode( $this->executeDeleteRequest( $this->getRequestUrl( $id, $query ), null ) ); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -87,7 +87,7 @@ public function sendByEmail( $id, $email = null ): mixed { array("email" => $email) )); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -110,7 +110,7 @@ public function downloadZip( $id ): string { try { return $this->executeGetRequest( $this->getRequestUrl( $id ) . "/zip" ); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -133,7 +133,7 @@ public function downloadPdf( $id ): string { try { return $this->executeGetRequest( $this->getRequestUrl( $id ) . "/pdf" ); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -156,7 +156,7 @@ public function downloadXml( $id ): string { try { return $this->executeGetRequest( $this->getRequestUrl( $id ) . "/xml" ); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -179,7 +179,7 @@ public function downloadCancellationReceiptXml( $id ): string { try { return $this->executeGetRequest( $this->getRequestUrl( $id . "/cancellation_receipt/xml" ) ); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -203,7 +203,7 @@ public function downloadCancellationReceiptPdf( $id ): string { try { return $this->executeGetRequest( $this->getRequestUrl( $id ) . "/cancellation_receipt/pdf" ); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -227,7 +227,7 @@ public function updateStatus( $id ): mixed { try { return json_decode( $this->executeJsonPutRequest( $this->getRequestUrl( $id . "/status" ), null ) ); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -251,7 +251,7 @@ public function updateDraft( $id, $body ): mixed { try { return json_decode( $this->executeJsonPutRequest( $this->getRequestUrl( $id ), $body ) ); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -275,7 +275,7 @@ public function stampDraft( $id, $query = null ): mixed { try { return json_decode( $this->executeJsonPostRequest( $this->getRequestUrl( $id . "/stamp", $query ) ) ); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -298,7 +298,7 @@ public function copyToDraft( $id ): mixed { try { return json_decode( $this->executeJsonPostRequest( $this->getRequestUrl( $id . "/copy" ), null ) ); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -321,7 +321,7 @@ public function previewPdf( $body ): string { try { return $this->executeJsonPostRequest( $this->getRequestUrl( "preview/pdf" ), $body ); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } diff --git a/src/Resources/Organizations.php b/src/Resources/Organizations.php index 56621f8..56e5521 100644 --- a/src/Resources/Organizations.php +++ b/src/Resources/Organizations.php @@ -23,7 +23,7 @@ public function all($params = null): mixed try { return json_decode($this->executeGetRequest($this->getRequestUrl($params))); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -40,7 +40,7 @@ public function retrieve($id): mixed try { return json_decode($this->executeGetRequest($this->getRequestUrl($id))); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -57,7 +57,7 @@ public function create($params): mixed try { return json_decode($this->executeJsonPostRequest($this->getRequestUrl(), $params)); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -75,7 +75,7 @@ public function updateLegal($id, $params): mixed try { return json_decode($this->executeJsonPutRequest($this->getRequestUrl($id) . "/legal", $params)); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -93,7 +93,7 @@ public function updateCustomization($id, $params): mixed try { return json_decode($this->executeJsonPutRequest($this->getRequestUrl($id) . "/customization", $params)); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -111,7 +111,7 @@ public function updateReceiptSettings($id, $params): mixed try { return json_decode($this->executeJsonPutRequest($this->getRequestUrl($id) . "/receipts", $params)); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -129,7 +129,7 @@ public function updateDomain($id, $params): mixed try { return json_decode($this->executeJsonPutRequest($this->getRequestUrl($id) . "/domain", $params)); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -151,7 +151,7 @@ public function checkDomainIsAvailable($id, $params): mixed ) ); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -169,7 +169,7 @@ public function uploadLogo($id, $params): mixed try { return json_decode($this->executeDataPutRequest($this->getRequestUrl($id) . "/logo", $params)); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -187,7 +187,7 @@ public function uploadCertificate($id, $params): mixed try { return json_decode($this->executeDataPutRequest($this->getRequestUrl($id) . "/certificate", $params)); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -204,7 +204,7 @@ public function getTestApiKey($id): mixed try { return json_decode($this->executeGetRequest($this->getRequestUrl($id) . "/apikeys/test")); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -226,7 +226,7 @@ public function renewTestApiKey($id): mixed ) ); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -261,7 +261,7 @@ public function listLiveApiKeys($id): mixed ) ); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -283,7 +283,7 @@ public function renewLiveApiKey($id): mixed ) ); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -306,7 +306,7 @@ public function deleteLiveApiKey($organizationId, $apiKeyId): mixed ) ); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -323,7 +323,7 @@ public function delete($id): mixed try { return json_decode($this->executeDeleteRequest($this->getRequestUrl($id), null)); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -340,7 +340,7 @@ public function deleteCertificate($id): mixed try { return json_decode($this->executeDeleteRequest($this->getRequestUrl($id) . "/certificate", null)); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -357,7 +357,7 @@ public function getSeriesGroup($id): mixed try { return json_decode($this->executeGetRequest($this->getRequestUrl($id) . "/series-group")); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -375,7 +375,7 @@ public function createSeriesGroup($id, $params): mixed try { return json_decode($this->executeJsonPostRequest($this->getRequestUrl($id) . "/series-group", $params)); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -394,7 +394,7 @@ public function updateSeriesGroup($id, $series_name, $params): mixed try { return json_decode($this->executeJsonPutRequest($this->getRequestUrl($id) . "/series-group" . "/" . $series_name, $params)); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -412,7 +412,7 @@ public function deleteSeriesGroup($id, $series_name): mixed try { return json_decode($this->executeDeleteRequest($this->getRequestUrl($id) . "/series-group" . "/" . $series_name, null)); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -430,7 +430,7 @@ public function updateSelfInvoiceSettings($id, $params): mixed try { return json_decode($this->executeJsonPutRequest($this->getRequestUrl($id) . "/self-invoice", $params)); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } } diff --git a/src/Resources/Products.php b/src/Resources/Products.php index 0144e6b..bd1c588 100644 --- a/src/Resources/Products.php +++ b/src/Resources/Products.php @@ -21,7 +21,7 @@ public function all( $params = null ): mixed { try { return json_decode( $this->executeGetRequest( $this->getRequestUrl( $params ) ) ); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -37,7 +37,7 @@ public function retrieve( $id ): mixed { try { return json_decode( $this->executeGetRequest( $this->getRequestUrl( $id ) ) ); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -53,7 +53,7 @@ public function create( $params ): mixed { try { return json_decode( $this->executeJsonPostRequest( $this->getRequestUrl(), $params ) ); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -71,7 +71,7 @@ public function update( $id, $params ): mixed { try { return json_decode( $this->executeJsonPutRequest( $this->getRequestUrl( $id ), $params ) ); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -87,7 +87,7 @@ public function delete( $id ): mixed { try { return json_decode( $this->executeDeleteRequest( $this->getRequestUrl( $id ), null ) ); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } diff --git a/src/Resources/Receipts.php b/src/Resources/Receipts.php index af7f8a2..06b7dc3 100644 --- a/src/Resources/Receipts.php +++ b/src/Resources/Receipts.php @@ -21,7 +21,7 @@ public function all( $params = null ): mixed { try { return json_decode( $this->executeGetRequest( $this->getRequestUrl( $params ) ) ); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -37,7 +37,7 @@ public function retrieve( $id ): mixed { try { return json_decode( $this->executeGetRequest( $this->getRequestUrl( $id ) ) ); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -53,7 +53,7 @@ public function create( $params ): mixed { try { return json_decode( $this->executeJsonPostRequest( $this->getRequestUrl(), $params ) ); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -71,7 +71,7 @@ public function invoice( $id, $params ): mixed { try { return json_decode( $this->executeJsonPostRequest( $this->getRequestUrl( $id ) . "/invoice", $params ) ); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -87,7 +87,7 @@ public function createGlobalInvoice( $params ): mixed { try { return json_decode( $this->executeJsonPostRequest( $this->getRequestUrl() . "/global-invoice", $params ) ); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -103,7 +103,7 @@ public function cancel( $id ): mixed { try { return json_decode( $this->executeDeleteRequest( $this->getRequestUrl( $id ), null ) ); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -123,7 +123,7 @@ public function sendByEmail( $id, $email = null ): mixed { $email == null ? null : array("email" => $email) )); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -147,7 +147,7 @@ public function downloadPdf( $id ): string { try { return $this->executeGetRequest( $this->getRequestUrl( $id ) . "/pdf" ); } catch ( FacturapiException $e ) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } diff --git a/src/Resources/Retentions.php b/src/Resources/Retentions.php index 9bb9cc3..63f7d47 100644 --- a/src/Resources/Retentions.php +++ b/src/Resources/Retentions.php @@ -23,7 +23,7 @@ public function all($params = null): mixed try { return json_decode($this->executeGetRequest($this->getRequestUrl($params))); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -40,7 +40,7 @@ public function retrieve($id): mixed try { return json_decode($this->executeGetRequest($this->getRequestUrl($id))); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -57,7 +57,7 @@ public function create($params): mixed try { return json_decode($this->executeJsonPostRequest($this->getRequestUrl(), $params)); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -76,7 +76,7 @@ public function cancel($id, $query): mixed try { return json_decode($this->executeDeleteRequest($this->getRequestUrl($id, $query), null)); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -97,7 +97,7 @@ public function sendByEmail($id, $email = null): mixed array("email" => $email) )); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -123,7 +123,7 @@ public function downloadZip($id): string try { return $this->executeGetRequest($this->getRequestUrl($id) . "/zip"); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -149,7 +149,7 @@ public function downloadPdf($id): string try { return $this->executeGetRequest($this->getRequestUrl($id) . "/pdf"); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -175,7 +175,7 @@ public function downloadXml($id): string try { return $this->executeGetRequest($this->getRequestUrl($id) . "/xml"); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } diff --git a/src/Resources/Tools.php b/src/Resources/Tools.php index 36f6a1a..75c13c7 100644 --- a/src/Resources/Tools.php +++ b/src/Resources/Tools.php @@ -26,7 +26,7 @@ public function validateTaxId($tax_id): mixed ) ); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } } diff --git a/src/Resources/Webhooks.php b/src/Resources/Webhooks.php index c7022ec..888a5b1 100644 --- a/src/Resources/Webhooks.php +++ b/src/Resources/Webhooks.php @@ -22,7 +22,7 @@ public function all($params = null): mixed try { return json_decode($this->executeGetRequest($this->getRequestUrl($params))); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -39,7 +39,7 @@ public function retrieve($id): mixed try { return json_decode($this->executeGetRequest($this->getRequestUrl($id))); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -56,7 +56,7 @@ public function create($params): mixed try { return json_decode($this->executeJsonPostRequest($this->getRequestUrl(), $params)); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -75,7 +75,7 @@ public function update($id, $params): mixed try { return json_decode($this->executeJsonPutRequest($this->getRequestUrl($id), $params)); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -92,7 +92,7 @@ public function delete($id): mixed try { return json_decode($this->executeDeleteRequest($this->getRequestUrl($id), null)); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } @@ -123,7 +123,7 @@ public function validateSignature($data): mixed try { return json_decode($this->executeJsonPostRequest($this->getRequestUrl() . '/validate-signature', $data)); } catch (FacturapiException $e) { - throw new FacturapiException($e->getMessage(), 0, $e); + throw $e; } } diff --git a/tests/Http/ErrorHandlingNonJsonEdgeCasesTest.php b/tests/Http/ErrorHandlingNonJsonEdgeCasesTest.php new file mode 100644 index 0000000..d622662 --- /dev/null +++ b/tests/Http/ErrorHandlingNonJsonEdgeCasesTest.php @@ -0,0 +1,75 @@ + 'application/json'], $rawBody) + ); + + $invoices = new Invoices('sk_test_abc123', ['httpClient' => $httpClient]); + + try { + $invoices->create(['customer' => []]); + self::fail('Expected FacturapiException to be thrown.'); + } catch (FacturapiException $exception) { + self::assertSame(500, $exception->getStatusCode()); + self::assertSame($rawBody, $exception->getMessage()); + self::assertNull($exception->getErrorData()); + self::assertSame($rawBody, $exception->getRawBody()); + } + } + + public function testEmptyErrorBodyIsExposedAsEmptyString(): void + { + $httpClient = new FakeHttpClient( + new Response(500, ['Content-Type' => 'text/plain'], '') + ); + + $invoices = new Invoices('sk_test_abc123', ['httpClient' => $httpClient]); + + try { + $invoices->create(['customer' => []]); + self::fail('Expected FacturapiException to be thrown.'); + } catch (FacturapiException $exception) { + self::assertSame(500, $exception->getStatusCode()); + self::assertSame('', $exception->getMessage()); + self::assertNull($exception->getErrorData()); + self::assertSame('', $exception->getRawBody()); + } + } + + public function testPlainTextErrorBodyIsPreserved(): void + { + $rawBody = 'gateway overloaded, retry later'; + + $httpClient = new FakeHttpClient( + new Response(503, ['Content-Type' => 'text/plain'], $rawBody) + ); + + $invoices = new Invoices('sk_test_abc123', ['httpClient' => $httpClient]); + + try { + $invoices->create(['customer' => []]); + self::fail('Expected FacturapiException to be thrown.'); + } catch (FacturapiException $exception) { + self::assertSame(503, $exception->getStatusCode()); + self::assertSame($rawBody, $exception->getMessage()); + self::assertNull($exception->getErrorData()); + self::assertSame($rawBody, $exception->getRawBody()); + } + } +} diff --git a/tests/Http/ErrorHandlingTest.php b/tests/Http/ErrorHandlingTest.php new file mode 100644 index 0000000..f98ffc5 --- /dev/null +++ b/tests/Http/ErrorHandlingTest.php @@ -0,0 +1,71 @@ + 'Request validation failed', + 'code' => 'validation_error', + 'details' => [ + [ + 'path' => 'customer.tax_id', + 'message' => 'customer.tax_id must be a valid RFC', + 'code' => 'invalid_rfc', + ], + ], + ]; + + $httpClient = new FakeHttpClient( + new Response(422, ['Content-Type' => 'application/json'], json_encode($errorBody)) + ); + + $invoices = new Invoices('sk_test_abc123', ['httpClient' => $httpClient]); + + try { + $invoices->create(['customer' => []]); + self::fail('Expected FacturapiException to be thrown.'); + } catch (FacturapiException $exception) { + self::assertSame(422, $exception->getStatusCode()); + self::assertSame('Request validation failed', $exception->getMessage()); + self::assertSame($errorBody, $exception->getErrorData()); + self::assertSame($errorBody, $exception->getResponseData()); + self::assertSame(json_encode($errorBody), $exception->getRawBody()); + + self::assertSame('validation_error', $exception->getErrorData()['code']); + self::assertSame('customer.tax_id', $exception->getErrorData()['details'][0]['path']); + self::assertSame('invalid_rfc', $exception->getErrorData()['details'][0]['code']); + } + } + + public function testNonJsonErrorsStillExposeRawBody(): void + { + $rawBody = '502 Bad Gateway'; + + $httpClient = new FakeHttpClient( + new Response(502, ['Content-Type' => 'text/html'], $rawBody) + ); + + $invoices = new Invoices('sk_test_abc123', ['httpClient' => $httpClient]); + + try { + $invoices->create(['customer' => []]); + self::fail('Expected FacturapiException to be thrown.'); + } catch (FacturapiException $exception) { + self::assertSame(502, $exception->getStatusCode()); + self::assertSame($rawBody, $exception->getMessage()); + self::assertNull($exception->getErrorData()); + self::assertSame($rawBody, $exception->getRawBody()); + } + } +} diff --git a/tests/Resources/InvoiceReceiptCreationTest.php b/tests/Resources/InvoiceReceiptCreationTest.php new file mode 100644 index 0000000..1b1c1ee --- /dev/null +++ b/tests/Resources/InvoiceReceiptCreationTest.php @@ -0,0 +1,70 @@ + $httpClient]); + + $payload = [ + 'customer' => 'cus_123', + 'items' => [['quantity' => 1]], + ]; + + $result = $invoices->create($payload, ['as' => 'draft', 'validate' => true]); + + self::assertSame('inv_123', $result->id); + + $request = $httpClient->requests()[0]; + self::assertSame('POST', $request->getMethod()); + self::assertSame('https://www.facturapi.io/v2/invoices?as=draft&validate=true', (string) $request->getUri()); + self::assertSame('application/json', $request->getHeaderLine('Content-Type')); + self::assertSame(json_encode($payload), (string) $request->getBody()); + } + + public function testReceiptsCreateSendsJsonBodyToExpectedEndpoint(): void + { + $httpClient = new FakeHttpClient(new Response(201, [], '{"id":"rec_123"}')); + $receipts = new Receipts('sk_test_abc123', ['httpClient' => $httpClient]); + + $payload = [ + 'customer' => 'cus_123', + 'items' => [['quantity' => 1]], + ]; + + $result = $receipts->create($payload); + + self::assertSame('rec_123', $result->id); + + $request = $httpClient->requests()[0]; + self::assertSame('POST', $request->getMethod()); + self::assertSame('https://www.facturapi.io/v2/receipts', (string) $request->getUri()); + self::assertSame('application/json', $request->getHeaderLine('Content-Type')); + self::assertSame(json_encode($payload), (string) $request->getBody()); + } + + public function testInvoicesCancelSerializesQueryParamsWithoutExtraSlash(): void + { + $httpClient = new FakeHttpClient(new Response(200, [], '{"status":"canceled"}')); + $invoices = new Invoices('sk_test_abc123', ['httpClient' => $httpClient]); + + $invoices->cancel('inv_123', ['motive' => '01', 'substitution' => 'inv_456']); + + $request = $httpClient->requests()[0]; + self::assertSame( + 'https://www.facturapi.io/v2/invoices/inv_123?motive=01&substitution=inv_456', + (string) $request->getUri() + ); + } +} diff --git a/tests/Resources/OrganizationsMultipartTest.php b/tests/Resources/OrganizationsMultipartTest.php index a63ff2b..1cf73d3 100644 --- a/tests/Resources/OrganizationsMultipartTest.php +++ b/tests/Resources/OrganizationsMultipartTest.php @@ -4,6 +4,7 @@ namespace Facturapi\Tests\Resources; +use Facturapi\Exceptions\FacturapiException; use Facturapi\Resources\Organizations; use Facturapi\Tests\Support\FakeHttpClient; use GuzzleHttp\Psr7\Response; @@ -69,4 +70,28 @@ public function testUploadCertificateBuildsMultipartRequestWithCerKeyAndPassword @unlink($tmpCer); @unlink($tmpKey); } + + public function testUploadCertificateFailsWhenRequiredFieldsAreMissing(): void + { + $httpClient = new FakeHttpClient(new Response(200, [], '{"ok":true}')); + $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + + $this->expectException(FacturapiException::class); + $this->expectExceptionMessage('Invalid certificate payload. Expected cerFile, keyFile and password.'); + + $organizations->uploadCertificate('org_123', [ + 'cerFile' => '/tmp/cert.cer', + ]); + } + + public function testUploadLogoFailsWhenFileCannotBeOpened(): void + { + $httpClient = new FakeHttpClient(new Response(200, [], '{"ok":true}')); + $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + + $this->expectException(FacturapiException::class); + $this->expectExceptionMessage('Unable to open file: /tmp/file_that_does_not_exist.pdf'); + + $organizations->uploadLogo('org_123', '/tmp/file_that_does_not_exist.pdf'); + } } From 2260abb7589703b265ce3d643ded3a6348c27f12 Mon Sep 17 00:00:00 2001 From: javorosas Date: Mon, 30 Mar 2026 13:20:20 +0200 Subject: [PATCH 09/16] docs: clarify constructor breaking change and error details shape --- README.es.md | 4 +++- README.md | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.es.md b/README.es.md index 921ac46..094cd6e 100644 --- a/README.es.md +++ b/README.es.md @@ -161,13 +161,15 @@ try { $facturapi->Invoices->create($payload); } catch (FacturapiException $e) { $status = $e->getStatusCode(); - $error = $e->getErrorData(); // Incluye campos como code, message, path/details cuando el API los retorna. + $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' => '...'] } ``` ## Notas de Migración (v4) 🔄 - La versión mínima de PHP ahora es `>=8.2`. +- Se eliminó el soporte para el argumento posicional `apiVersion` en el constructor. - Los aliases snake_case están deprecados en v4 y se eliminarán en v5. - `Facturapi\\Exceptions\\Facturapi_Exception` está deprecada en v4 y se eliminará en v5. - Usa `Facturapi\\Exceptions\\FacturapiException`. diff --git a/README.md b/README.md index 7d57ca1..04b8f5f 100644 --- a/README.md +++ b/README.md @@ -161,13 +161,15 @@ try { $facturapi->Invoices->create($payload); } catch (FacturapiException $e) { $status = $e->getStatusCode(); - $error = $e->getErrorData(); // Includes fields like code, message, path/details when returned by API. + $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' => '...'] } ``` ## Migration Notes (v4) 🔄 - Minimum PHP version is now `>=8.2`. +- Removed support for the positional `apiVersion` constructor argument. - Snake_case method aliases are deprecated in v4 and will be removed in v5. - `Facturapi\\Exceptions\\Facturapi_Exception` is deprecated in v4 and will be removed in v5. - Use `Facturapi\\Exceptions\\FacturapiException`. From 3ddbd224c0d195fed985553b4ceffd90f07d4170 Mon Sep 17 00:00:00 2001 From: javorosas Date: Mon, 30 Mar 2026 13:22:02 +0200 Subject: [PATCH 10/16] fix: apply Copilot review findings for error consistency and autoload --- src/Facturapi.php | 18 ----------- src/Http/BaseClient.php | 4 +-- .../Resources/InvoiceReceiptCreationTest.php | 26 ++++++++++++++++ .../Resources/OrganizationsMultipartTest.php | 30 +++++++++++++++++++ 4 files changed, 58 insertions(+), 20 deletions(-) diff --git a/src/Facturapi.php b/src/Facturapi.php index f57a6da..4d44075 100644 --- a/src/Facturapi.php +++ b/src/Facturapi.php @@ -2,24 +2,6 @@ namespace Facturapi; -require_once 'Http/BaseClient.php'; -require_once 'Exceptions/FacturapiException.php'; -require_once 'InvoiceRelation.php'; -require_once 'InvoiceType.php'; -require_once 'PaymentForm.php'; -require_once 'TaxType.php'; -require_once 'Resources/Customers.php'; -require_once 'Resources/Organizations.php'; -require_once 'Resources/Products.php'; -require_once 'Resources/Invoices.php'; -require_once 'Resources/Receipts.php'; -require_once 'Resources/Catalogs.php'; -require_once 'Resources/CartaPorteCatalogs.php'; -require_once 'Resources/ComercioExteriorCatalogs.php'; -require_once 'Resources/Retentions.php'; -require_once 'Resources/Tools.php'; -require_once 'Resources/Webhooks.php'; - use Facturapi\Resources\Customers; use Facturapi\Resources\Organizations; use Facturapi\Resources\Products; diff --git a/src/Http/BaseClient.php b/src/Http/BaseClient.php index 74219b3..9759c47 100644 --- a/src/Http/BaseClient.php +++ b/src/Http/BaseClient.php @@ -269,7 +269,7 @@ protected function executeDataPutRequest($url, $body): string 'Content-Type' => 'multipart/form-data; boundary=' . $multipart->getBoundary(), ); - return $this->executeRequest('PUT', $url, $headers, $multipart, false); + return $this->executeRequest('PUT', $url, $headers, $multipart, true); } /** @@ -285,7 +285,7 @@ protected function executeDataPutRequest($url, $body): string protected function executeDeleteRequest($url, $body): string { $payload = $body === null ? null : (string) $body; - return $this->executeRequest('DELETE', $url, array('Content-Type' => 'application/json'), $payload, false); + return $this->executeRequest('DELETE', $url, array('Content-Type' => 'application/json'), $payload, true); } /** diff --git a/tests/Resources/InvoiceReceiptCreationTest.php b/tests/Resources/InvoiceReceiptCreationTest.php index 1b1c1ee..9c6e547 100644 --- a/tests/Resources/InvoiceReceiptCreationTest.php +++ b/tests/Resources/InvoiceReceiptCreationTest.php @@ -4,6 +4,7 @@ namespace Facturapi\Tests\Resources; +use Facturapi\Exceptions\FacturapiException; use Facturapi\Resources\Invoices; use Facturapi\Resources\Receipts; use Facturapi\Tests\Support\FakeHttpClient; @@ -67,4 +68,29 @@ public function testInvoicesCancelSerializesQueryParamsWithoutExtraSlash(): void (string) $request->getUri() ); } + + public function testInvoicesCancelThrowsOnNon2xxAndPreservesErrorShape(): void + { + $errorBody = [ + 'message' => 'Cancellation rejected', + 'code' => 'cancel_error', + 'details' => [ + ['path' => 'motive', 'message' => 'Invalid motive'], + ], + ]; + + $httpClient = new FakeHttpClient( + new Response(409, ['Content-Type' => 'application/json'], json_encode($errorBody)) + ); + $invoices = new Invoices('sk_test_abc123', ['httpClient' => $httpClient]); + + try { + $invoices->cancel('inv_123', ['motive' => '99']); + self::fail('Expected FacturapiException to be thrown.'); + } catch (FacturapiException $exception) { + self::assertSame(409, $exception->getStatusCode()); + self::assertSame('Cancellation rejected', $exception->getMessage()); + self::assertSame($errorBody, $exception->getErrorData()); + } + } } diff --git a/tests/Resources/OrganizationsMultipartTest.php b/tests/Resources/OrganizationsMultipartTest.php index 1cf73d3..5fd3bc4 100644 --- a/tests/Resources/OrganizationsMultipartTest.php +++ b/tests/Resources/OrganizationsMultipartTest.php @@ -94,4 +94,34 @@ public function testUploadLogoFailsWhenFileCannotBeOpened(): void $organizations->uploadLogo('org_123', '/tmp/file_that_does_not_exist.pdf'); } + + public function testUploadLogoThrowsOnNon2xxAndPreservesErrorShape(): void + { + $tmpLogo = tempnam(sys_get_temp_dir(), 'logo_'); + file_put_contents($tmpLogo, 'LOGO_BYTES'); + + $errorBody = [ + 'message' => 'Upload failed', + 'code' => 'upload_error', + 'details' => [ + ['path' => 'file', 'message' => 'Invalid file'], + ], + ]; + + $httpClient = new FakeHttpClient( + new Response(422, ['Content-Type' => 'application/json'], json_encode($errorBody)) + ); + $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + + try { + $organizations->uploadLogo('org_123', $tmpLogo); + self::fail('Expected FacturapiException to be thrown.'); + } catch (FacturapiException $exception) { + self::assertSame(422, $exception->getStatusCode()); + self::assertSame('Upload failed', $exception->getMessage()); + self::assertSame($errorBody, $exception->getErrorData()); + } finally { + @unlink($tmpLogo); + } + } } From 320dc5ce0beaff25131e854663bcfbc09a0c0624 Mon Sep 17 00:00:00 2001 From: javorosas Date: Mon, 30 Mar 2026 13:50:14 +0200 Subject: [PATCH 11/16] chore(v4): keep non-composer support and rename VERSION to VERSION.md --- .github/workflows/deploy-on-main.yml | 16 ++++++++-------- README.es.md | 8 ++++++++ README.md | 8 ++++++++ VERSION => VERSION.md | 5 ++++- src/Facturapi.php | 21 +++++++++++++++++++++ 5 files changed, 49 insertions(+), 9 deletions(-) rename VERSION => VERSION.md (83%) diff --git a/.github/workflows/deploy-on-main.yml b/.github/workflows/deploy-on-main.yml index 3c1552d..8236d54 100644 --- a/.github/workflows/deploy-on-main.yml +++ b/.github/workflows/deploy-on-main.yml @@ -25,15 +25,15 @@ jobs: run: | set -euo pipefail - if [[ ! -f VERSION ]]; then - echo "Missing VERSION file" + if [[ ! -f VERSION.md ]]; then + echo "Missing VERSION.md file" exit 1 fi - LOCAL_VERSION="$(head -n1 VERSION | tr -d '[:space:]')" - RELEASE_BODY="$(tail -n +2 VERSION || true)" + LOCAL_VERSION="$(head -n1 VERSION.md | tr -d '[:space:]')" + RELEASE_BODY="$(tail -n +2 VERSION.md || true)" if [[ ! "$LOCAL_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then - echo "VERSION must be valid semver. Got: $LOCAL_VERSION" + echo "VERSION.md first line must be valid semver. Got: $LOCAL_VERSION" exit 1 fi @@ -61,13 +61,13 @@ jobs: HIGHEST="$(printf '%s\n%s\n' "$REMOTE_MAX" "$LOCAL_VERSION" | sort -V | tail -n1)" if [[ "$HIGHEST" != "$LOCAL_VERSION" ]]; then - echo "Local VERSION is not greater than remote max tag" + echo "Local VERSION.md is not greater than remote max tag" echo "should_deploy=false" >> "$GITHUB_OUTPUT" exit 0 fi if ! printf '%s' "$RELEASE_BODY" | grep -q '[^[:space:]]'; then - echo "VERSION requires release notes after the first line when publishing." + echo "VERSION.md requires release notes after the first line when publishing." echo "Example:" echo " 3.6.1" echo " ## What changed" @@ -108,4 +108,4 @@ jobs: - name: Deploy skipped if: steps.version.outputs.should_deploy != 'true' run: | - echo "Skipping deploy: local VERSION is not greater than remote semver tag." + echo "Skipping deploy: local VERSION.md is not greater than remote semver tag." diff --git a/README.es.md b/README.es.md index 094cd6e..c522d93 100644 --- a/README.es.md +++ b/README.es.md @@ -16,6 +16,12 @@ Idioma: Español | [English](./README.md) composer require facturapi/facturapi-php ``` +Sin Composer (workaround soportado): + +```php +require_once __DIR__ . '/path/to/facturapi-php/src/Facturapi.php'; +``` + Requisitos: - PHP `>=8.2` @@ -170,6 +176,8 @@ try { - La versión mínima de PHP ahora es `>=8.2`. - Se eliminó el soporte para el argumento posicional `apiVersion` en el constructor. +- Proyectos con Composer: no requieren cambios de carga; continúen usando `vendor/autoload.php`. +- Proyectos sin Composer pueden seguir usando el SDK cargando `src/Facturapi.php` directamente. - Los aliases snake_case están deprecados en v4 y se eliminarán en v5. - `Facturapi\\Exceptions\\Facturapi_Exception` está deprecada en v4 y se eliminará en v5. - Usa `Facturapi\\Exceptions\\FacturapiException`. diff --git a/README.md b/README.md index 04b8f5f..0f00ab8 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,12 @@ Language: English | [Español](./README.es.md) composer require facturapi/facturapi-php ``` +Without Composer (supported workaround): + +```php +require_once __DIR__ . '/path/to/facturapi-php/src/Facturapi.php'; +``` + Requirements: - PHP `>=8.2` @@ -170,6 +176,8 @@ try { - Minimum PHP version is now `>=8.2`. - Removed support for the positional `apiVersion` constructor argument. +- Composer projects: no loader changes needed; keep using `vendor/autoload.php`. +- Non-Composer projects can keep using the SDK by loading `src/Facturapi.php` directly. - Snake_case method aliases are deprecated in v4 and will be removed in v5. - `Facturapi\\Exceptions\\Facturapi_Exception` is deprecated in v4 and will be removed in v5. - Use `Facturapi\\Exceptions\\FacturapiException`. diff --git a/VERSION b/VERSION.md similarity index 83% rename from VERSION rename to VERSION.md index b39d38f..b75cb02 100644 --- a/VERSION +++ b/VERSION.md @@ -1,6 +1,7 @@ 4.0.0 -## Breaking +## 🚨 Breaking Changes +- Impact: update required only if you use PHP < 8.2 or relied on the positional `apiVersion` constructor argument. - Bumped minimum supported PHP version to `>=8.2`. - Simplified SDK constructor to a single optional `config` parameter: - `apiVersion` (defaults to `v2`) @@ -12,6 +13,8 @@ - You can update without code changes if: - Your project already runs on PHP `>=8.2`. - You instantiate the SDK as `new Facturapi($apiKey)` (single API key argument). + - If you use Composer, keep loading `vendor/autoload.php` as usual. + - If you do not use Composer, load `src/Facturapi.php` directly. ## Deprecations (Non-Breaking) - Snake_case aliases remain functional in v4, but are deprecated and will be removed in v5. diff --git a/src/Facturapi.php b/src/Facturapi.php index 4d44075..4d7a034 100644 --- a/src/Facturapi.php +++ b/src/Facturapi.php @@ -2,6 +2,27 @@ namespace Facturapi; +// Backward-compatible loading path for projects that still include src/Facturapi.php directly. +// Composer/PSR-4 users can continue relying on autoloading. +require_once __DIR__ . '/Http/BaseClient.php'; +require_once __DIR__ . '/Exceptions/FacturapiException.php'; +require_once __DIR__ . '/Exceptions/Facturapi_Exception.php'; +require_once __DIR__ . '/InvoiceRelation.php'; +require_once __DIR__ . '/InvoiceType.php'; +require_once __DIR__ . '/PaymentForm.php'; +require_once __DIR__ . '/TaxType.php'; +require_once __DIR__ . '/Resources/Customers.php'; +require_once __DIR__ . '/Resources/Organizations.php'; +require_once __DIR__ . '/Resources/Products.php'; +require_once __DIR__ . '/Resources/Invoices.php'; +require_once __DIR__ . '/Resources/Receipts.php'; +require_once __DIR__ . '/Resources/Catalogs.php'; +require_once __DIR__ . '/Resources/CartaPorteCatalogs.php'; +require_once __DIR__ . '/Resources/ComercioExteriorCatalogs.php'; +require_once __DIR__ . '/Resources/Retentions.php'; +require_once __DIR__ . '/Resources/Tools.php'; +require_once __DIR__ . '/Resources/Webhooks.php'; + use Facturapi\Resources\Customers; use Facturapi\Resources\Organizations; use Facturapi\Resources\Products; From 3c1725012d0da1115ab3055ba6aef8ee4a09fe69 Mon Sep 17 00:00:00 2001 From: javorosas Date: Mon, 30 Mar 2026 14:12:14 +0200 Subject: [PATCH 12/16] fix: address meaningful Copilot review comments --- src/Exceptions/Facturapi_Exception.php | 5 -- src/Http/BaseClient.php | 31 ++++++++--- src/Resources/Organizations.php | 12 ++++- tests/Resources/InvoicesTest.php | 9 ++++ tests/Resources/OrganizationsDomainTest.php | 59 +++++++++++++++++++++ tests/Support/FakeHttpClient.php | 4 +- 6 files changed, 105 insertions(+), 15 deletions(-) create mode 100644 tests/Resources/OrganizationsDomainTest.php diff --git a/src/Exceptions/Facturapi_Exception.php b/src/Exceptions/Facturapi_Exception.php index ebb9af2..4f5b0d4 100644 --- a/src/Exceptions/Facturapi_Exception.php +++ b/src/Exceptions/Facturapi_Exception.php @@ -4,11 +4,6 @@ require_once __DIR__ . '/FacturapiException.php'; -trigger_error( - 'Facturapi\\Exceptions\\Facturapi_Exception is deprecated and will be removed in v5. Use Facturapi\\Exceptions\\FacturapiException instead.', - E_USER_DEPRECATED -); - if (!class_exists(__NAMESPACE__ . '\\Facturapi_Exception', false)) { class_alias(FacturapiException::class, __NAMESPACE__ . '\\Facturapi_Exception'); } diff --git a/src/Http/BaseClient.php b/src/Http/BaseClient.php index 9759c47..3591d77 100644 --- a/src/Http/BaseClient.php +++ b/src/Http/BaseClient.php @@ -76,11 +76,11 @@ public function __construct(string $apiKey, ?array $config = null) /** * Gets the status code from the most recent HTTP request. * - * @return int + * @return int|null */ - public function getLastStatus(): int + public function getLastStatus(): ?int { - return (int) $this->lastStatus; + return $this->lastStatus; } /** @@ -234,20 +234,27 @@ protected function executeJsonPutRequest($url, $body): string */ protected function executeDataPutRequest($url, $body): string { + $openStreams = array(); + if (is_array($body)) { if (!isset($body['cerFile'], $body['keyFile'], $body['password'])) { throw new FacturapiException('Invalid certificate payload. Expected cerFile, keyFile and password.'); } + $cerStream = $this->openFileStream($body['cerFile']); + $keyStream = $this->openFileStream($body['keyFile']); + $openStreams[] = $cerStream; + $openStreams[] = $keyStream; + $multipart = new MultipartStream(array( array( 'name' => 'cer', - 'contents' => Utils::streamFor($this->openFileStream($body['cerFile'])), + 'contents' => Utils::streamFor($cerStream), 'filename' => basename($body['cerFile']) ), array( 'name' => 'key', - 'contents' => Utils::streamFor($this->openFileStream($body['keyFile'])), + 'contents' => Utils::streamFor($keyStream), 'filename' => basename($body['keyFile']) ), array( @@ -256,10 +263,12 @@ protected function executeDataPutRequest($url, $body): string ), )); } else { + $fileStream = $this->openFileStream($body); + $openStreams[] = $fileStream; $multipart = new MultipartStream(array( array( 'name' => 'file', - 'contents' => Utils::streamFor($this->openFileStream($body)), + 'contents' => Utils::streamFor($fileStream), 'filename' => basename($body) ), )); @@ -269,7 +278,15 @@ protected function executeDataPutRequest($url, $body): string 'Content-Type' => 'multipart/form-data; boundary=' . $multipart->getBoundary(), ); - return $this->executeRequest('PUT', $url, $headers, $multipart, true); + try { + return $this->executeRequest('PUT', $url, $headers, $multipart, true); + } finally { + foreach ($openStreams as $stream) { + if (is_resource($stream)) { + fclose($stream); + } + } + } } /** diff --git a/src/Resources/Organizations.php b/src/Resources/Organizations.php index 56e5521..a9e1c30 100644 --- a/src/Resources/Organizations.php +++ b/src/Resources/Organizations.php @@ -136,13 +136,12 @@ public function updateDomain($id, $params): mixed /** * Check domain availability. * - * @param string $id Organization ID. * @param array $params Domain check parameters. * @return mixed JSON-decoded response. * * @throws FacturapiException */ - public function checkDomainIsAvailable($id, $params): mixed + public function checkDomainAvailability($params): mixed { try { return json_decode( @@ -155,6 +154,15 @@ public function checkDomainIsAvailable($id, $params): mixed } } + /** + * @deprecated The $id parameter is ignored. Use checkDomainAvailability($params) instead. + */ + public function checkDomainIsAvailable($id, $params): mixed + { + trigger_error('Organizations::checkDomainIsAvailable($id, $params) is deprecated and will be removed in v5. Use checkDomainAvailability($params) instead.', E_USER_DEPRECATED); + return $this->checkDomainAvailability($params); + } + /** * Uploads the organization's logo * diff --git a/tests/Resources/InvoicesTest.php b/tests/Resources/InvoicesTest.php index 087a5b5..190ffe5 100644 --- a/tests/Resources/InvoicesTest.php +++ b/tests/Resources/InvoicesTest.php @@ -11,6 +11,14 @@ final class InvoicesTest extends TestCase { + public function testGetLastStatusIsNullBeforeAnyRequest(): void + { + $httpClient = new FakeHttpClient(new Response(200, [], '{}')); + $invoices = new Invoices('sk_test_abc123', ['httpClient' => $httpClient]); + + self::assertNull($invoices->getLastStatus()); + } + public function testDownloadPdfUsesExpectedPathAndAuthorizationHeader(): void { $httpClient = new FakeHttpClient(new Response(200, [], 'PDF_BINARY_CONTENT')); @@ -25,5 +33,6 @@ public function testDownloadPdfUsesExpectedPathAndAuthorizationHeader(): void self::assertSame('https://www.facturapi.io/v2/invoices/inv_123/pdf', (string) $request->getUri()); self::assertSame('Basic ' . base64_encode('sk_test_abc123:'), $request->getHeaderLine('Authorization')); self::assertSame('facturapi-php', $request->getHeaderLine('User-Agent')); + self::assertSame(200, $invoices->getLastStatus()); } } diff --git a/tests/Resources/OrganizationsDomainTest.php b/tests/Resources/OrganizationsDomainTest.php new file mode 100644 index 0000000..8b5d2b0 --- /dev/null +++ b/tests/Resources/OrganizationsDomainTest.php @@ -0,0 +1,59 @@ + $httpClient]); + + $result = $organizations->checkDomainAvailability([ + 'name' => 'acme', + 'domain' => 'acme.mx', + ]); + + self::assertTrue($result->available); + + $request = $httpClient->requests()[0]; + self::assertSame('GET', $request->getMethod()); + self::assertSame( + 'https://www.facturapi.io/v2/organizations/domain-check?name=acme&domain=acme.mx', + (string) $request->getUri() + ); + } + + public function testLegacyCheckDomainIsAvailableDelegatesAndWarns(): void + { + $httpClient = new FakeHttpClient(new Response(200, [], '{"available":true}')); + $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + + $captured = []; + set_error_handler(static function (int $severity, string $message) use (&$captured): bool { + $captured[] = ['severity' => $severity, 'message' => $message]; + return true; + }); + + try { + $result = $organizations->checkDomainIsAvailable('org_ignored', [ + 'name' => 'acme', + 'domain' => 'acme.mx', + ]); + } finally { + restore_error_handler(); + } + + self::assertTrue($result->available); + self::assertNotEmpty($captured); + self::assertSame(E_USER_DEPRECATED, $captured[0]['severity']); + self::assertStringContainsString('checkDomainAvailability($params)', $captured[0]['message']); + } +} diff --git a/tests/Support/FakeHttpClient.php b/tests/Support/FakeHttpClient.php index f13a2c2..6bfc0d7 100644 --- a/tests/Support/FakeHttpClient.php +++ b/tests/Support/FakeHttpClient.php @@ -9,6 +9,7 @@ use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use RuntimeException; +use GuzzleHttp\Psr7\Utils; final class FakeHttpClient implements ClientInterface { @@ -25,7 +26,8 @@ public function __construct(ResponseInterface ...$responses) public function sendRequest(RequestInterface $request): ResponseInterface { - $this->requests[] = $request; + $bodySnapshot = (string) $request->getBody(); + $this->requests[] = $request->withBody(Utils::streamFor($bodySnapshot)); if ($this->responses === []) { throw new FakeHttpClientException('No fake response queued.'); From 13be80128c5c3ddbb44dcff5f6c2410d9b037039 Mon Sep 17 00:00:00 2001 From: javorosas Date: Mon, 30 Mar 2026 14:24:07 +0200 Subject: [PATCH 13/16] fix: align organization domain-check method with docs --- src/Resources/Organizations.php | 21 +++++++---- tests/Resources/OrganizationsDomainTest.php | 39 ++++++++++----------- 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/src/Resources/Organizations.php b/src/Resources/Organizations.php index a9e1c30..b1ebc43 100644 --- a/src/Resources/Organizations.php +++ b/src/Resources/Organizations.php @@ -141,12 +141,22 @@ public function updateDomain($id, $params): mixed * * @throws FacturapiException */ - public function checkDomainAvailability($params): mixed + public function checkDomainIsAvailable($idOrParams, $params = null): mixed { + $query = null; + if (is_array($idOrParams) && $params === null) { + $query = $idOrParams; + } elseif (is_array($params)) { + // Backward compatibility with historical signature: ($id, $params) + $query = $params; + } else { + throw new FacturapiException('checkDomainIsAvailable expects either ($params) or ($id, $params).'); + } + try { return json_decode( $this->executeGetRequest( - $this->getRequestUrl("domain-check", $params) + $this->getRequestUrl("domain-check", $query) ) ); } catch (FacturapiException $e) { @@ -155,12 +165,11 @@ public function checkDomainAvailability($params): mixed } /** - * @deprecated The $id parameter is ignored. Use checkDomainAvailability($params) instead. + * Alias for consistency with API operation naming. */ - public function checkDomainIsAvailable($id, $params): mixed + public function checkDomainAvailability($params): mixed { - trigger_error('Organizations::checkDomainIsAvailable($id, $params) is deprecated and will be removed in v5. Use checkDomainAvailability($params) instead.', E_USER_DEPRECATED); - return $this->checkDomainAvailability($params); + return $this->checkDomainIsAvailable($params); } /** diff --git a/tests/Resources/OrganizationsDomainTest.php b/tests/Resources/OrganizationsDomainTest.php index 8b5d2b0..a3083a5 100644 --- a/tests/Resources/OrganizationsDomainTest.php +++ b/tests/Resources/OrganizationsDomainTest.php @@ -11,12 +11,12 @@ final class OrganizationsDomainTest extends TestCase { - public function testCheckDomainAvailabilityUsesExpectedEndpoint(): void + public function testCheckDomainIsAvailableUsesExpectedEndpoint(): void { $httpClient = new FakeHttpClient(new Response(200, [], '{"available":true}')); $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); - $result = $organizations->checkDomainAvailability([ + $result = $organizations->checkDomainIsAvailable([ 'name' => 'acme', 'domain' => 'acme.mx', ]); @@ -31,29 +31,28 @@ public function testCheckDomainAvailabilityUsesExpectedEndpoint(): void ); } - public function testLegacyCheckDomainIsAvailableDelegatesAndWarns(): void + public function testCheckDomainIsAvailableAcceptsLegacyTwoArgumentSignature(): void { $httpClient = new FakeHttpClient(new Response(200, [], '{"available":true}')); $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + $result = $organizations->checkDomainIsAvailable('org_ignored', [ + 'name' => 'acme', + 'domain' => 'acme.mx', + ]); - $captured = []; - set_error_handler(static function (int $severity, string $message) use (&$captured): bool { - $captured[] = ['severity' => $severity, 'message' => $message]; - return true; - }); - - try { - $result = $organizations->checkDomainIsAvailable('org_ignored', [ - 'name' => 'acme', - 'domain' => 'acme.mx', - ]); - } finally { - restore_error_handler(); - } + self::assertTrue($result->available); + } + + public function testCheckDomainAvailabilityAliasDelegatesToCanonicalMethod(): void + { + $httpClient = new FakeHttpClient(new Response(200, [], '{"available":true}')); + $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + + $result = $organizations->checkDomainAvailability([ + 'name' => 'acme', + 'domain' => 'acme.mx', + ]); self::assertTrue($result->available); - self::assertNotEmpty($captured); - self::assertSame(E_USER_DEPRECATED, $captured[0]['severity']); - self::assertStringContainsString('checkDomainAvailability($params)', $captured[0]['message']); } } From 7cf699d5efd3afaffeff5fe79108f9a34e1e22f6 Mon Sep 17 00:00:00 2001 From: javorosas Date: Mon, 30 Mar 2026 14:31:30 +0200 Subject: [PATCH 14/16] fix: deprecate legacy domain-check signature and relax gzip header --- src/Http/BaseClient.php | 2 +- src/Resources/Organizations.php | 8 +++++--- tests/Resources/OrganizationsDomainTest.php | 22 +++++++++++++++++---- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/Http/BaseClient.php b/src/Http/BaseClient.php index 3591d77..d7baae3 100644 --- a/src/Http/BaseClient.php +++ b/src/Http/BaseClient.php @@ -147,7 +147,7 @@ protected function getRequestUrl($params = null, $query = null): string */ protected function executeGetRequest($url): string { - return $this->executeRequest('GET', $url, array('Accept-Encoding' => 'gzip')); + return $this->executeRequest('GET', $url); } /** diff --git a/src/Resources/Organizations.php b/src/Resources/Organizations.php index b1ebc43..0d520e7 100644 --- a/src/Resources/Organizations.php +++ b/src/Resources/Organizations.php @@ -143,11 +143,13 @@ public function updateDomain($id, $params): mixed */ public function checkDomainIsAvailable($idOrParams, $params = null): mixed { + $argsCount = func_num_args(); $query = null; - if (is_array($idOrParams) && $params === null) { + if ($argsCount === 1 && is_array($idOrParams)) { $query = $idOrParams; - } elseif (is_array($params)) { - // Backward compatibility with historical signature: ($id, $params) + } elseif ($argsCount >= 2 && is_array($params)) { + // Backward compatibility with historical signature: ($id, $params). + trigger_error('Organizations::checkDomainIsAvailable($id, $params) is deprecated and will be removed in v5. Use checkDomainIsAvailable($params) instead.', E_USER_DEPRECATED); $query = $params; } else { throw new FacturapiException('checkDomainIsAvailable expects either ($params) or ($id, $params).'); diff --git a/tests/Resources/OrganizationsDomainTest.php b/tests/Resources/OrganizationsDomainTest.php index a3083a5..666691a 100644 --- a/tests/Resources/OrganizationsDomainTest.php +++ b/tests/Resources/OrganizationsDomainTest.php @@ -35,12 +35,26 @@ public function testCheckDomainIsAvailableAcceptsLegacyTwoArgumentSignature(): v { $httpClient = new FakeHttpClient(new Response(200, [], '{"available":true}')); $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); - $result = $organizations->checkDomainIsAvailable('org_ignored', [ - 'name' => 'acme', - 'domain' => 'acme.mx', - ]); + + $captured = []; + set_error_handler(static function (int $severity, string $message) use (&$captured): bool { + $captured[] = ['severity' => $severity, 'message' => $message]; + return true; + }); + + try { + $result = $organizations->checkDomainIsAvailable('org_ignored', [ + 'name' => 'acme', + 'domain' => 'acme.mx', + ]); + } finally { + restore_error_handler(); + } self::assertTrue($result->available); + self::assertNotEmpty($captured); + self::assertSame(E_USER_DEPRECATED, $captured[0]['severity']); + self::assertStringContainsString('checkDomainIsAvailable($params)', $captured[0]['message']); } public function testCheckDomainAvailabilityAliasDelegatesToCanonicalMethod(): void From e634ccc7624c550cf2ba288e8f822dabe37f9d7a Mon Sep 17 00:00:00 2001 From: javorosas Date: Mon, 30 Mar 2026 14:46:49 +0200 Subject: [PATCH 15/16] ci: run phpunit on pull requests and pushes to main --- .github/workflows/ci.yml | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0a70b1b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + tests: + name: PHP ${{ matrix.php-version }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php-version: ['8.2', '8.4'] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + coverage: none + tools: composer:v2 + + - name: Install dependencies + run: composer install --no-interaction --prefer-dist + + - name: Run test suite + run: ./vendor/bin/phpunit --configuration phpunit.xml From 1a8841b6f88f0d056c305b35739e32d54abe6d1a Mon Sep 17 00:00:00 2001 From: javorosas Date: Mon, 30 Mar 2026 15:06:56 +0200 Subject: [PATCH 16/16] ci: add workflow_dispatch trigger to CI workflow --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a70b1b..d1f2f33 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ on: branches: - main pull_request: + workflow_dispatch: permissions: contents: read