diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..d1f2f33
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,37 @@
+name: CI
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ workflow_dispatch:
+
+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
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
new file mode 100644
index 0000000..c522d93
--- /dev/null
+++ b/README.es.md
@@ -0,0 +1,192 @@
+# Facturapi PHP SDK
+
+SDK oficial de PHP para [Facturapi](https://www.facturapi.io).
+
+Idioma: Español | [English](./README.md)
+
+[](https://packagist.org/packages/facturapi/facturapi-php)
+[](https://packagist.org/packages/facturapi/facturapi-php)
+[](https://packagist.org/packages/facturapi/facturapi-php)
+[](https://packagist.org/packages/facturapi/facturapi-php)
+[](https://packagist.org/packages/facturapi/facturapi-php)
+
+## Instalación ⚡
+
+```bash
+composer require facturapi/facturapi-php
+```
+
+Sin Composer (workaround soportado):
+
+```php
+require_once __DIR__ . '/path/to/facturapi-php/src/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,
+]);
+```
+
+## 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(); // 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.
+- 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`.
+
+## 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 82fc6fb..0f00ab8 100644
--- a/README.md
+++ b/README.md
@@ -1,149 +1,192 @@
-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).
+Language: English | [Español](./README.es.md)
-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.
+[](https://packagist.org/packages/facturapi/facturapi-php)
+[](https://packagist.org/packages/facturapi/facturapi-php)
+[](https://packagist.org/packages/facturapi/facturapi-php)
+[](https://packagist.org/packages/facturapi/facturapi-php)
+[](https://packagist.org/packages/facturapi/facturapi-php)
-## Install
+## Installation ⚡
```bash
-composer require "facturapi/facturapi-php"
+composer require facturapi/facturapi-php
```
-## Before you begin
+Without Composer (supported workaround):
-Make sure you have created your free account on [FacturAPI](https://www.facturapi.io) and that you have your **API Keys**.
-
-## Getting started
+```php
+require_once __DIR__ . '/path/to/facturapi-php/src/Facturapi.php';
+```
-### Import the library
+Requirements:
+- PHP `>=8.2`
-Don't forget to reference the library at the top of your code:
+## 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',
+ ],
+]);
```
-### Create a customer
+## Client Configuration ⚙️
+
+Constructor signature:
```php
+new Facturapi(string $apiKey, ?array $config = null)
+```
-// 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);
+Supported config keys:
+- `apiVersion` (`string`, default: `v2`)
+- `timeout` (`int|float`, default: `360` seconds)
+- `httpClient` (`Psr\Http\Client\ClientInterface`, advanced)
+
+Example:
+
+```php
+use Facturapi\Facturapi;
+
+$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 );
+$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',
+]);
+```
+
+### Download Files
-$facturapi->Invoices->download_zip("INVOICE_ID") // stream containing the PDF and XML as a ZIP file or
+```php
+$zipBytes = $facturapi->Invoices->downloadZip('INVOICE_ID');
+$pdfBytes = $facturapi->Invoices->downloadPdf('INVOICE_ID');
+$xmlBytes = $facturapi->Invoices->downloadXml('INVOICE_ID');
+```
-$facturapi->Invoices->download_pdf("INVOICE_ID") // stream containing the PDF file or
+`downloadPdf()` returns raw PDF bytes (binary string), not base64.
-$facturapi->Invoices->download_xml("INVOICE_ID") // stream containing the XML file or
+```php
+file_put_contents('invoice.pdf', $pdfBytes);
```
-#### Send your invoice by email
+### Send by Email
```php
-// Send the invoice to your customer's email (if any)
-$facturapi = new Facturapi( FACTURAPI_KEY );
+$facturapi->Invoices->sendByEmail('INVOICE_ID');
+```
+
+### Comercio Exterior Catalogs
-$facturapi->Invoices->send_by_email("INVOICE_ID");
+```php
+$results = $facturapi->ComercioExteriorCatalogs->searchTariffFractions([
+ 'q' => '0101',
+ 'page' => 0,
+ 'limit' => 10,
+]);
```
-## Documentation
+## Error Handling ⚠️
-There's more you can do with this library: List, retrieve, update, and remove Customers, Products and Invoices.
+On non-2xx responses, the SDK throws `Facturapi\Exceptions\FacturapiException`.
-Visit the full documentation at http://docs.facturapi.io.
+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.
-## Help
+```php
+use Facturapi\Exceptions\FacturapiException;
+
+try {
+ $facturapi->Invoices->create($payload);
+} catch (FacturapiException $e) {
+ $status = $e->getStatusCode();
+ $error = $e->getErrorData(); // Full API error shape when body is valid JSON.
+ $firstDetail = $error['details'][0] ?? null; // e.g. ['path' => 'items.0.quantity', 'message' => '...', 'code' => '...']
+}
+```
-### Found a bug?
+## Migration Notes (v4) 🔄
-Please report it on the Issue Tracker
+- 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`.
-### 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
deleted file mode 100644
index c8c4a70..0000000
--- a/VERSION
+++ /dev/null
@@ -1,5 +0,0 @@
-3.7.0
-
-## What changed
-- Added `ComercioExteriorCatalogs::searchTariffFractions` support for Comercio Exterior catalog queries.
-- Added deploy automation from `main` using semver + release notes defined in `VERSION`.
diff --git a/VERSION.md b/VERSION.md
new file mode 100644
index 0000000..b75cb02
--- /dev/null
+++ b/VERSION.md
@@ -0,0 +1,31 @@
+4.0.0
+
+## 🚨 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`)
+ - `timeout` (seconds)
+ - `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).
+ - 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.
+- `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.
+- Renamed exception class to `FacturapiException` and kept `Facturapi_Exception` as a compatibility alias.
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/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/Exceptions/FacturapiException.php b/src/Exceptions/FacturapiException.php
new file mode 100644
index 0000000..5ad4f83
--- /dev/null
+++ b/src/Exceptions/FacturapiException.php
@@ -0,0 +1,47 @@
+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/Exceptions/Facturapi_Exception.php b/src/Exceptions/Facturapi_Exception.php
index ca7c53b..4f5b0d4 100644
--- a/src/Exceptions/Facturapi_Exception.php
+++ b/src/Exceptions/Facturapi_Exception.php
@@ -2,7 +2,8 @@
namespace Facturapi\Exceptions;
+require_once __DIR__ . '/FacturapiException.php';
-use Exception;
-
-class Facturapi_Exception extends Exception {}
\ No newline at end of file
+if (!class_exists(__NAMESPACE__ . '\\Facturapi_Exception', false)) {
+ class_alias(FacturapiException::class, __NAMESPACE__ . '\\Facturapi_Exception');
+}
diff --git a/src/Facturapi.php b/src/Facturapi.php
index 9564bee..4d7a034 100644
--- a/src/Facturapi.php
+++ b/src/Facturapi.php
@@ -2,22 +2,26 @@
namespace Facturapi;
-require_once 'Http/BaseClient.php';
-require_once 'Exceptions/Facturapi_Exception.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/Retentions.php';
-require_once 'Resources/Tools.php';
-require_once 'Resources/Webhooks.php';
+// 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;
@@ -26,35 +30,43 @@
use Facturapi\Resources\Receipts;
use Facturapi\Resources\Catalogs;
use Facturapi\Resources\CartaPorteCatalogs;
+use Facturapi\Resources\ComercioExteriorCatalogs;
use Facturapi\Resources\Retentions;
use Facturapi\Resources\Tools;
use Facturapi\Resources\Webhooks;
+use Psr\Http\Client\ClientInterface;
class Facturapi
{
+ public Customers $Customers;
+ public Organizations $Organizations;
+ public Products $Products;
+ public Invoices $Invoices;
+ public Receipts $Receipts;
+ public Catalogs $Catalogs;
+ public CartaPorteCatalogs $CartaPorteCatalogs;
+ public ComercioExteriorCatalogs $ComercioExteriorCatalogs;
+ public Retentions $Retentions;
+ public Tools $Tools;
+ public Webhooks $Webhooks;
- public $Customers;
- public $Organizations;
- public $Products;
- public $Invoices;
- public $Receipts;
- public $Catalogs;
- public $CartaPorteCatalogs;
- public $Retentions;
- public $Tools;
- public $Webhooks;
-
- public function __construct($api_key, $api_version = 'v2')
+ /**
+ * @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.
+ */
+ public function __construct(string $apiKey, ?array $config = null)
{
- $this->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..d7baae3 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,22 +50,37 @@ 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|null
*/
- public function getLastStatus()
+ public function getLastStatus(): ?int
{
- return (int) $this->lastStatus;
+ return $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,444 @@ 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);
}
/**
* 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
+ * @param string $url URL to call.
+ * @param mixed $body Request body.
+ * @param bool $formenc Whether to send as form-urlencoded.
+ * @return string Response body.
*
- * @return Body of request result
- *
- * @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
+ * @param string $url URL to call.
+ * @param array|null $body Request body.
+ * @return string Response body.
*
- * @return String Body of request result
- *
- * @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)
- );
+ $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.');
+ }
- $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);
+ $cerStream = $this->openFileStream($body['cerFile']);
+ $keyStream = $this->openFileStream($body['keyFile']);
+ $openStreams[] = $cerStream;
+ $openStreams[] = $keyStream;
+
+ $multipart = new MultipartStream(array(
+ array(
+ 'name' => 'cer',
+ 'contents' => Utils::streamFor($cerStream),
+ 'filename' => basename($body['cerFile'])
+ ),
+ array(
+ 'name' => 'key',
+ 'contents' => Utils::streamFor($keyStream),
+ 'filename' => basename($body['keyFile'])
+ ),
+ array(
+ 'name' => 'password',
+ 'contents' => (string) $body['password']
+ ),
+ ));
} else {
- return $result;
+ $fileStream = $this->openFileStream($body);
+ $openStreams[] = $fileStream;
+ $multipart = new MultipartStream(array(
+ array(
+ 'name' => 'file',
+ 'contents' => Utils::streamFor($fileStream),
+ 'filename' => basename($body)
+ ),
+ ));
+ }
+
+ $headers = array(
+ 'Content-Type' => 'multipart/form-data; boundary=' . $multipart->getBoundary(),
+ );
+
+ try {
+ return $this->executeRequest('PUT', $url, $headers, $multipart, true);
+ } finally {
+ foreach ($openStreams as $stream) {
+ if (is_resource($stream)) {
+ fclose($stream);
+ }
+ }
}
}
/**
* 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, true);
}
/**
* 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);
+ }
+
+ /**
+ * 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 executeRequest($method, $url, $headers = array(), $body = null, $validateStatus = true): string
+ {
+ $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)) {
+ $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;
}
/**
- * Sets the status code from a curl request
+ * 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
*/
- protected function setLastStatusFromCurl($ch)
+ private function extractErrorMessage(?array $errorData, string $rawBody): string
{
- $info = curl_getinfo($ch);
- $this->lastStatus = (isset($info['http_code'])) ? $info['http_code'] : null;
+ 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.
+ *
+ * @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..1fe95d4 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 $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 $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 $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 $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 $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 $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 $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 $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 $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 $e;
}
}
}
diff --git a/src/Resources/Catalogs.php b/src/Resources/Catalogs.php
index 2ebb111..b74eacb 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 $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 $e;
}
}
}
diff --git a/src/Resources/ComercioExteriorCatalogs.php b/src/Resources/ComercioExteriorCatalogs.php
index 37c524e..2016eea 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 $e;
}
}
}
diff --git a/src/Resources/Customers.php b/src/Resources/Customers.php
index e8eeef2..c4ed04a 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 $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 $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 $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 $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 $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 $e;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Resources/Invoices.php b/src/Resources/Invoices.php
index 88541e8..21fbf06 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 $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 $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 $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 $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 $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 $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 $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 $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 $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 $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 $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 $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 $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 $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 $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..0d520e7 100644
--- a/src/Resources/Organizations.php
+++ b/src/Resources/Organizations.php
@@ -3,462 +3,453 @@
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 $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 $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 $e;
}
}
/**
- * Update a Organization's legal information
- *
- * @param $id
- * @param $params array of properties and property values for Organization's legal information
- *
- * @return Response body from HTTP PUT request
+ * Update an organization's legal information.
*
- * @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 $e;
}
}
/**
- * Update a Organization's customization information
+ * Update an 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
- *
- * @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 $e;
}
}
/**
- * Update a 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
+ * Update an organization's receipt settings.
*
- * @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 $e;
}
}
/**
- * Update the Organization's domain
+ * Update an organization's domain.
*
- * @param $id Organization Id
- * @param $params array of properties and property values for the 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 $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 array $params Domain check parameters.
+ * @return mixed JSON-decoded response.
*
+ * @throws FacturapiException
*/
- public function checkDomainIsAvailable($id, $params)
+ public function checkDomainIsAvailable($idOrParams, $params = null): mixed
{
+ $argsCount = func_num_args();
+ $query = null;
+ if ($argsCount === 1 && is_array($idOrParams)) {
+ $query = $idOrParams;
+ } 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).');
+ }
+
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", $query)
)
);
- } catch (Facturapi_Exception $e) {
- throw new Facturapi_Exception('Unable to check domain\'s availability: ' . $e->getMessage());
+ } catch (FacturapiException $e) {
+ throw $e;
}
}
+ /**
+ * Alias for consistency with API operation naming.
+ */
+ public function checkDomainAvailability($params): mixed
+ {
+ return $this->checkDomainIsAvailable($params);
+ }
+
/**
* 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 $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 $e;
}
}
/**
- * Get the Test Api Key for an Organization
- *
- * @param id : Unique ID for Organization
+ * Get 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 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 $e;
}
}
/**
- * Renews the Test Api Key for an Organization and makes the previous one inactive
+ * Renew 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 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 $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.
+ *
+ * @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.
*
- * @return Array Array of object with first_12 characters, created_at field and id field
+ * @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 $e;
}
}
-
/**
- * Renews the Test Api Key for an Organization and makes the previous one inactive
- *
- * @param id : Unique ID for Organization
+ * Renew the live API key for an organization.
*
- * @return String Live Api Key
+ * @param string $id Organization ID.
+ * @return mixed JSON-decoded response.
*
- * @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 $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 $e;
}
}
/**
- * Delete a Organization
+ * Delete an organization.
*
- * @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 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 $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 $e;
}
}
/**
- * Get the series of an Organization
+ * Get series groups for an organization.
*
- * @param id : Unique ID for the Organization
+ * @param string $id Organization ID.
+ * @return mixed JSON-decoded response.
*
- * @return Response body from HTTP GET request
- *
- * @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 $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 $e;
}
}
/**
- * Update 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 updated Series Organization
+ * Update a series group for an organization.
*
- * @return Response body with JSON object
- * for updated Series Organization from HTTP POST request
+ * @param string $id Organization ID.
+ * @param string $series_name Series name.
+ * @param array $params Series group payload.
+ * @return mixed JSON-decoded response.
*
- * @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 $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 $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 $e;
}
}
}
diff --git a/src/Resources/Products.php b/src/Resources/Products.php
index 0921c9b..bd1c588 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 $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 $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 $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 $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 $e;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Resources/Receipts.php b/src/Resources/Receipts.php
index 6913854..06b7dc3 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 $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 $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 $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 $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 $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 $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 $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 $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..63f7d47 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 $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 $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 $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 $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 $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 $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 $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 $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..75c13c7 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 $e;
}
}
-
-}
\ No newline at end of file
+}
diff --git a/src/Resources/Webhooks.php b/src/Resources/Webhooks.php
index 23ced20..888a5b1 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 $e;
}
}
/**
* Get a Webhook by ID
*
- * @param id : Unique ID for webhook
+ * @param string $id Webhook ID.
+ * @return mixed JSON-decoded response.
*
- * @return JSON object for requested Webhook
- *
- * @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 $e;
}
}
/**
* Create a Webhook in your organization
*
- * @param params : array of properties and property values for new webhook
+ * @param array $params Webhook payload.
+ * @return mixed JSON-decoded response.
*
- * @return Response body with JSON object
- * for created Webhook 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 webhook: ' . $e->getMessage());
+ return json_decode($this->executeJsonPostRequest($this->getRequestUrl(), $params));
+ } catch (FacturapiException $e) {
+ throw $e;
}
}
@@ -68,52 +64,98 @@ 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 $e;
}
}
/**
* Delete a Webhook in your organization
*
- * @param id : Unique ID for the webhook
+ * @param string $id Webhook 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 webhook: ' . $e->getMessage());
+ return json_decode($this->executeDeleteRequest($this->getRequestUrl($id), null));
+ } catch (FacturapiException $e) {
+ throw $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.
+ *
+ * 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)
+ 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->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 $e;
+ }
+ }
+
+ /**
+ * 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.
+ */
+ private 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/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..9c6e547
--- /dev/null
+++ b/tests/Resources/InvoiceReceiptCreationTest.php
@@ -0,0 +1,96 @@
+ $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()
+ );
+ }
+
+ 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/InvoicesTest.php b/tests/Resources/InvoicesTest.php
new file mode 100644
index 0000000..190ffe5
--- /dev/null
+++ b/tests/Resources/InvoicesTest.php
@@ -0,0 +1,38 @@
+ $httpClient]);
+
+ self::assertNull($invoices->getLastStatus());
+ }
+
+ public function testDownloadPdfUsesExpectedPathAndAuthorizationHeader(): void
+ {
+ $httpClient = new FakeHttpClient(new Response(200, [], 'PDF_BINARY_CONTENT'));
+ $invoices = new Invoices('sk_test_abc123', ['httpClient' => $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'));
+ self::assertSame(200, $invoices->getLastStatus());
+ }
+}
diff --git a/tests/Resources/OrganizationsDomainTest.php b/tests/Resources/OrganizationsDomainTest.php
new file mode 100644
index 0000000..666691a
--- /dev/null
+++ b/tests/Resources/OrganizationsDomainTest.php
@@ -0,0 +1,72 @@
+ $httpClient]);
+
+ $result = $organizations->checkDomainIsAvailable([
+ '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 testCheckDomainIsAvailableAcceptsLegacyTwoArgumentSignature(): 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('checkDomainIsAvailable($params)', $captured[0]['message']);
+ }
+
+ 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);
+ }
+}
diff --git a/tests/Resources/OrganizationsMultipartTest.php b/tests/Resources/OrganizationsMultipartTest.php
new file mode 100644
index 0000000..5fd3bc4
--- /dev/null
+++ b/tests/Resources/OrganizationsMultipartTest.php
@@ -0,0 +1,127 @@
+ $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);
+ }
+
+ 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');
+ }
+
+ 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);
+ }
+ }
+}
diff --git a/tests/Resources/WebhooksTest.php b/tests/Resources/WebhooksTest.php
new file mode 100644
index 0000000..2d16234
--- /dev/null
+++ b/tests/Resources/WebhooksTest.php
@@ -0,0 +1,79 @@
+ $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]);
+
+ $payload = [
+ 'body' => '{"id":"evt_123"}',
+ 'signature' => 'sha256=fake_signature',
+ // No webhookSecret -> triggers API fallback
+ ];
+
+ $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 testValidateSignatureLocalSupportsRawHexAndRejectsInvalidSignatures(): void
+ {
+ $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);
+
+ $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());
+ }
+}
diff --git a/tests/Support/FakeHttpClient.php b/tests/Support/FakeHttpClient.php
new file mode 100644
index 0000000..6bfc0d7
--- /dev/null
+++ b/tests/Support/FakeHttpClient.php
@@ -0,0 +1,48 @@
+ */
+ private array $requests = [];
+
+ /** @var list */
+ private array $responses;
+
+ public function __construct(ResponseInterface ...$responses)
+ {
+ $this->responses = $responses;
+ }
+
+ public function sendRequest(RequestInterface $request): ResponseInterface
+ {
+ $bodySnapshot = (string) $request->getBody();
+ $this->requests[] = $request->withBody(Utils::streamFor($bodySnapshot));
+
+ 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 @@
+