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) + +[![Última Versión](https://img.shields.io/packagist/v/facturapi/facturapi-php?style=flat-square)](https://packagist.org/packages/facturapi/facturapi-php) +[![Versión de PHP](https://img.shields.io/packagist/php-v/facturapi/facturapi-php?style=flat-square)](https://packagist.org/packages/facturapi/facturapi-php) +[![Descargas Totales](https://img.shields.io/packagist/dt/facturapi/facturapi-php?style=flat-square)](https://packagist.org/packages/facturapi/facturapi-php) +[![Descargas Mensuales](https://img.shields.io/packagist/dm/facturapi/facturapi-php?style=flat-square)](https://packagist.org/packages/facturapi/facturapi-php) +[![Licencia](https://img.shields.io/packagist/l/facturapi/facturapi-php?style=flat-square)](https://packagist.org/packages/facturapi/facturapi-php) + +## Instalación ⚡ + +```bash +composer require facturapi/facturapi-php +``` + +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. +[![Latest Version](https://img.shields.io/packagist/v/facturapi/facturapi-php?style=flat-square)](https://packagist.org/packages/facturapi/facturapi-php) +[![PHP Version](https://img.shields.io/packagist/php-v/facturapi/facturapi-php?style=flat-square)](https://packagist.org/packages/facturapi/facturapi-php) +[![Total Downloads](https://img.shields.io/packagist/dt/facturapi/facturapi-php?style=flat-square)](https://packagist.org/packages/facturapi/facturapi-php) +[![Monthly Downloads](https://img.shields.io/packagist/dm/facturapi/facturapi-php?style=flat-square)](https://packagist.org/packages/facturapi/facturapi-php) +[![License](https://img.shields.io/packagist/l/facturapi/facturapi-php?style=flat-square)](https://packagist.org/packages/facturapi/facturapi-php) -## 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 @@ +