diff --git a/.agents/skills/create-api-resource/SKILL.md b/.agents/skills/create-api-resource/SKILL.md new file mode 100644 index 0000000..7c35da1 --- /dev/null +++ b/.agents/skills/create-api-resource/SKILL.md @@ -0,0 +1,46 @@ +--- +name: create-api-resource +description: Generates a new API Resource (DTOs, Resource Class, Methods, and Pest tests) using the official Facturapi OpenAPI specification. +--- + +# Create API Resource Skill + +Use this skill whenever you need to add a new API resource or endpoint to the Facturapi PHP SDK. + +## 1. Single Source of Truth (OpenAPI Spec) + +**CRITICAL RULE:** Do NOT search for a local `.yaml` file. The SDK does not ship with a downloaded OpenAPI spec to prevent it from becoming stale. + +You MUST dynamically fetch the live OpenAPI specification using the following URL: +`https://docs.facturapi.io/redocusaurus/api-es.yaml` + +Use your `read_url_content` or HTTP fetching tool to read the schema. + +## 2. Generate the DTO (Data Transfer Object) + +Once you understand the schema for the requested resource: +- Create a strictly typed DTO class inside `src/Models/`. +- The class must enforce `declare(strict_types=1);`. +- All properties should map to the JSON response fields exactly, but they must be strongly typed (e.g., `public string $id`, `public ?array $address`). +- Avoid using associative arrays for the return models. All new endpoints must return these strictly typed DTOs. + +## 3. Generate the Resource Class + +- Create a new Resource class in `src/Resources/` (e.g., `src/Resources/NewResource.php`). +- Extend `BaseResource` if applicable, or accept `FacturapiHttpClient` in the constructor. +- Add the necessary CRUD methods (`create`, `retrieve`, `all`, `delete`, etc.). +- Ensure all methods return the corresponding DTO or an array of DTOs, NEVER generic arrays. +- Handle nested structures carefully (e.g. `$response['data']` -> loop -> instantiate DTOs). + +## 4. Register the Endpoint + +- Open `src/Facturapi.php`. +- Add a new method (not a property!) to expose the new resource (e.g., `public function newResource(): NewResource`). +- Ensure the method utilizes lazy instantiation to avoid memory bloat. + +## 5. Scaffold Tests + +- Create a new Pest test file in `tests/` (e.g., `tests/NewResourceTest.php`). +- Use Pest v3's Expectation API (`expect()->toBe()`). Do NOT use PHPUnit assertions. +- Use the global helper `mockResponse(string $body, int $status = 200)` to mock the API responses instead of relying on a live network connection. +- Group the tests in a logical `describe('NewResource', function () { ... });` block. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d1f2f33..6acb02b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: CI +name: CI Strict Linter & Tests on: push: @@ -7,18 +7,48 @@ on: pull_request: workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + permissions: contents: read jobs: + lint-and-analyze: + name: Lint & Static Analysis + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + coverage: none + tools: composer:v2 + + - name: Install dependencies + uses: ramsey/composer-install@v3 + + - name: Audit dependencies + run: composer audit + + - name: Check Code Style (Pint) + run: composer format:test + + - name: Static Analysis (PHPStan) + run: composer analyze + tests: name: PHP ${{ matrix.php-version }} + needs: lint-and-analyze runs-on: ubuntu-latest strategy: fail-fast: false matrix: - php-version: ['8.2', '8.4'] - + php-version: ['8.2', '8.3', '8.4', '8.5'] steps: - name: Checkout uses: actions/checkout@v4 @@ -31,7 +61,7 @@ jobs: tools: composer:v2 - name: Install dependencies - run: composer install --no-interaction --prefer-dist + uses: ramsey/composer-install@v3 - - name: Run test suite - run: ./vendor/bin/phpunit --configuration phpunit.xml + - name: Run test suite (Pest) + run: composer test diff --git a/.github/workflows/deploy-on-main.yml b/.github/workflows/deploy-on-main.yml index 2437f85..6f4adac 100644 --- a/.github/workflows/deploy-on-main.yml +++ b/.github/workflows/deploy-on-main.yml @@ -97,6 +97,22 @@ jobs: git tag "$TAG" git push origin "$TAG" + - name: Build Standalone ZIP + if: steps.version.outputs.should_deploy == 'true' + run: | + mkdir -p build-standalone + cp -r src build-standalone/ + cp composer.json build-standalone/ + + cd build-standalone + composer require guzzlehttp/guzzle --no-interaction + composer install --no-dev --optimize-autoloader --no-interaction + + echo " autoload.php + + zip -r ../facturapi-php-standalone.zip src vendor autoload.php + cd .. + - name: Create GitHub release id: release if: steps.version.outputs.should_deploy == 'true' @@ -105,6 +121,7 @@ jobs: tag_name: ${{ steps.version.outputs.tag }} name: ${{ steps.version.outputs.tag }} body_path: RELEASE_NOTES.md + files: facturapi-php-standalone.zip - name: Notify Slack (success) if: steps.version.outputs.should_deploy == 'true' && steps.release.outcome == 'success' diff --git a/.gitignore b/.gitignore index 02a0e55..eadf379 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,8 @@ composer.lock /.idea/ .DS_Store .phpunit.cache/ + +# temporary files +/tmp/ +/tmps/ +/build/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..85a7b18 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,79 @@ +# Antigravity Agents Guidelines (AGENTS.md) + +Welcome to the **Facturapi PHP SDK** project. This file provides critical context, coding standards, and instructions for AI agents working on this codebase. + +## 1. Project Context & Domain Overview + +- **Project**: `facturapi/facturapi-php` +- **Domain**: Official PHP SDK for [Facturapi](https://www.facturapi.io), a Mexican electronic invoicing (CFDI) API. +- **Architecture**: A standard SDK composition model. The main entry point is the `Facturapi\Facturapi` class, which acts as a factory that instantiates and exposes various API resources (e.g., Customers, Invoices, Products). + +## 2. Tech Stack & Coding Standards + +- **Language**: PHP >= 8.2 +- **Autoloading**: PSR-4 (`Facturapi\` mapped to `src/`, `Facturapi\Tests\` mapped to `tests/`) +- **Strict Typing**: Use strict types always (`declare(strict_types=1);` at the top of every file). All properties, parameters, and returns must be strongly typed. +- **Style**: Follow PSR-12 standard conventions. +- **Dependencies**: Managed via Composer (`composer.json`). The SDK must remain completely framework-agnostic. +- **Static Analysis**: Enforced at **PHPStan Level 9** (`composer run analyze`). Code additions or refactors must pass with 0 errors without introducing artificial `ignoreErrors` in `phpstan.neon`. + +## 3. Important SDK Concepts (Architecture) + +- **HTTP Client**: + - The SDK relies on PSR-18 (HTTP Client) and PSR-17 (HTTP Factories). + - Internally, all requests are routed through `Facturapi\Http\FacturapiHttpClient`. + - The client is strictly **framework-agnostic** and uses `php-http/discovery` to automatically find available HTTP clients in the user's project (like Guzzle or Symfony HTTP). Do not tightly couple code to Guzzle. +- **Exceptions**: + - Errors from the API are intercepted and wrapped by the SDK into a **granular exception hierarchy**. + - All exceptions extend `Facturapi\Exceptions\FacturapiException`. + - Specific errors throw their respective child classes (e.g., `BadRequestException` for 400, `UnauthorizedException` for 401, `UnprocessableEntityException` for 422). +- **DTOs and Enums (Strictly Typed)**: + - **IMPORTANT**: The SDK v5 migration is COMPLETE. ALL API endpoints now return strictly typed Data Transfer Objects (DTOs) and Enums. + - Agents MUST NEVER write code assuming associative arrays for resource responses. Always use object property syntax (e.g., `$customer->legal_name`). + - **Backward Compatibility via `\ArrayAccess`**: Certain nested DTOs (e.g., `CommonAddressProperties`) intentionally implement `\ArrayAccess` to ensure seamless backward compatibility with legacy array-style access (e.g., `$customer->address['zip']`). Agents must preserve these interfaces and not remove them. +- **API Endpoints (Methods vs Properties)**: + - API interactions are handled through resource classes located in `src/Resources/`. + - Resource classes MUST be accessed via **methods** (e.g., `$facturapi->customers()`, `$facturapi->invoices()`). + - DO NOT use magic properties (e.g., `$facturapi->Customers`), as this syntax is deprecated and will trigger an undefined property error. +- **Statelessness (Long-Running Processes)**: + - The SDK is 100% compatible with long-running processes (FrankenPHP, RoadRunner, Swoole) and may be resolved as a Singleton in frameworks like Laravel Octane. + - Agents MUST NOT introduce request-specific state into the `Facturapi` or `FacturapiHttpClient` classes, as this will cause memory leaks or cross-tenant data bleed. + +## 4. Testing Instructions (Pest v3) + +- **Framework**: We strictly use **Pest v3** (`pestphp/pest`). PHPUnit is completely deprecated in this project. +- **Configuration & Setup**: + - The test suite uses `tests/TestCase.php` and `tests/Pest.php` to globally inject a `FakeHttpClient` and a `Facturapi` instance into every test via `setUp()`. + - Inside any test, the API is available as `test()->facturapi` (or `$this->facturapi`). +- **Mocking Responses**: + - Do NOT manually instantiate `FakeHttpClient`. + - Always use the global helper function `mockResponse(string $body, int $status = 200, array $headers = [])` at the beginning of the test. +- **Assertions (Expectation API)**: + - Do NOT use static PHPUnit assertions (like `self::assertSame` or `self::assertTrue`). + - You MUST use Pest's Expectation API: `expect($result)->toBe(...)`, `expect($array)->toBeArray()`, `expect($string)->toContain(...)`. +- **Exception Testing**: + - Do NOT use `try/catch` blocks or `self::fail()` to test exceptions. + - Use Pest's `toThrow` method passing a closure to validate internal exception data: + ```php + expect(fn () => $invoices->cancel('inv_123')) + ->toThrow(function (FacturapiException $exception) { + expect($exception->getStatusCode())->toBe(409); + }); + ``` +- **Grouping**: Always group related endpoints inside logical `describe('ResourceName', function () { ... });` blocks. + +## 5. Overview of `.agents` Skills + +This project contains customized AI skills to automate repetitive tasks and enforce SDK architectural standards. When working on related tasks, agents should refer to these skills: + +- **SDK Maintenance & Code Generation**: `create-api-resource` + _Use this skill whenever you need to add a new API endpoint, DTO, and Pest tests to the SDK. It explains how to scaffold the entire pipeline._ + +**CRITICAL - OpenAPI Single Source of Truth**: +The SDK does NOT store a local `.yaml` file. Whenever you need to read the Facturapi OpenAPI specification, you MUST fetch it directly from the live documentation site: +`https://docs.facturapi.io/redocusaurus/api-es.yaml` +Do not attempt to search for `tmp/api-es.yaml` or any local variant. + +--- + +CO-CREATED BY INCREDE < @increde - @macarthuror> diff --git a/CHANGELOG.md b/CHANGELOG.md index ce68529..90233f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [5.0.0] - 2026-10-11 + +### Changed + +- **BREAKING CHANGE**: All API resources now return strictly typed DTOs and Enums instead of generic associative arrays. +- **BREAKING CHANGE**: API resources are now accessed via methods (e.g. `$facturapi->customers()`) instead of magic properties (e.g. `$facturapi->Customers`). +- **BREAKING CHANGE**: Changed the internal HTTP client implementation (requires PSR-18 compatible client). +- **BREAKING CHANGE**: Required PHP version is strictly enforced to PHP >= 8.2. + ## [4.8.0] 2026-09-04 ### Added diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8f7ae4b --- /dev/null +++ b/Makefile @@ -0,0 +1,30 @@ +.PHONY: install test test-coverage clean format analyze lint pre-commit + +# Instala las dependencias del proyecto usando Composer +install: + composer install + +# Corre todos los tests con Pest +test: + ./vendor/bin/pest + +# Corre todos los tests mostrando el reporte de cobertura (requiere PCOV o Xdebug habilitado) +test-coverage: + ./vendor/bin/pest --coverage + +# Elimina las dependencias instaladas (útil para limpiar el entorno) +clean: + rm -rf vendor composer.lock + +# Comandos de linting y análisis +format: + composer format + +analyze: + composer analyze + +lint: + composer lint + +# Corre las herramientas de formato, análisis y tests (ideal para antes de un commit) +pre-commit: format analyze test diff --git a/README.es.md b/README.es.md index 6392837..d5d0bb3 100644 --- a/README.es.md +++ b/README.es.md @@ -9,29 +9,55 @@ Idioma: Español | [English](./README.md) [![Descargas Totales](https://img.shields.io/packagist/dt/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 ⚡ +## Compatibilidad ✅ -```bash -composer require facturapi/facturapi-php +- **PHP**: `>= 8.2` +- **Cliente HTTP PSR-18**: Totalmente compatible (se descubre automáticamente usando `php-http/discovery`). +- **Procesos de Larga Duración**: 100% compatible con servidores de aplicaciones como **FrankenPHP**, **RoadRunner** y **Swoole** (ej. Laravel Octane). El SDK no mantiene estado, por lo que es seguro y no introduce memory leaks. + +## Experiencia de Desarrollo (DX) ✨ + +La versión 5 introduce **DTOs (Data Transfer Objects) Estrictamente Tipados**. +En lugar de tener que adivinar las llaves de un arreglo, tu IDE ahora te ofrecerá autocompletado total (IntelliSense) para todas las respuestas de la API, haciendo el desarrollo más rápido y reduciendo significativamente los errores tipográficos. + +```php +// ❌ v4 (Arreglos Genéricos - Sin autocompletado) +echo $customer['legal_name']; + +// ✅ v5 (DTOs Tipados - Autocompletado total en el IDE) +echo $customer->legal_name; ``` -Sin Composer (workaround soportado): +### Soporte de Enums +El SDK incluye PHP Enums para catálogos comunes (ej. `PaymentForm`, `TaxType`). **Su uso es completamente opcional** (puedes seguir pasando strings crudos como `'01'`), pero son altamente recomendados. Los Enums no solo habilitan el autocompletado en el IDE y previenen errores de escritura, sino que también le dan contexto claro a los asistentes de IA (LLMs) sobre qué valores son válidos para inyectar en las peticiones. ```php -require_once __DIR__ . '/path/to/facturapi-php/src/Facturapi.php'; +// ✅ Opcional pero recomendado (Amigable para IDEs e IA) +'payment_form' => \Facturapi\PaymentForm::EFECTIVO, + +// 🆗 También válido +'payment_form' => '01', ``` -Requisitos: -- PHP `>=8.2` +## Instalación ⚡ -## Compatibilidad con Versiones Anteriores de PHP +```bash +composer require facturapi/facturapi-php +``` + +Sin Composer: + +1. Descarga el archivo `facturapi-php-standalone.zip` desde el [Último Release](https://github.com/facturapi/facturapi-php/releases/latest). +2. Extráelo en tu proyecto. +3. Importa el autoloader incluido: -Desde la versión **4.0.0**, el SDK requiere **PHP >= 8.2**. +```php +require_once __DIR__ . '/ruta/a/la/carpeta/extraida/autoload.php'; +``` -- Si tu proyecto ya usa PHP 8.2+, no necesitas cambios adicionales. -- Si tu proyecto está en PHP 8.1 o menor, fija la versión **3.7.0** (última release compatible antes del requisito de PHP 8.2). +## Configuración del Cliente ⚙️ -## Inicio Rápido 🚀 +Inicializa el SDK pasando tu API Key. ```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: +Opcionalmente, puedes pasar un arreglo de configuración como segundo argumento: ```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, + 'timeout' => 360, // segundos ]); ``` ### 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: +El SDK funciona directamente con su cliente interno basado en Guzzle. Si deseas proveer tu propio cliente compatible con PSR-18 (ej. para configurar proxies o timeouts personalizados): ```php use Facturapi\Facturapi; use GuzzleHttp\Client; -$httpClient = new Client([ - 'timeout' => 420, -]); +$httpClient = new Client(['timeout' => 420]); $facturapi = new Facturapi($apiKey, [ 'httpClient' => $httpClient, ]); ``` -## Uso Común 🧾 +## Ejemplos de Integración con Frameworks 📚 -### Crear un Producto +Revisa nuestras guías oficiales para integrar Facturapi con los frameworks de PHP más populares usando inyección de dependencias y las mejores prácticas: -```php -$product = $facturapi->Products->create([ - 'product_key' => '4319150114', - 'description' => 'Apple iPhone 8', - 'price' => 345.60, -]); -``` +- [Integración con Laravel](./docs/laravel.md) +- [Integración con Symfony](./docs/symfony.md) +- [Integración con CodeIgniter 4](./docs/codeigniter.md) -### Crear una Factura +--- +## Uso Principal 🧾 + +### 1. Facturas + +**Crear una Factura** ```php -$invoice = $facturapi->Invoices->create([ - 'customer' => 'YOUR_CUSTOMER_ID', +$invoice = $facturapi->invoices()->create([ + 'customer' => 'ID_DE_TU_CLIENTE', 'items' => [[ 'quantity' => 1, - 'product' => 'YOUR_PRODUCT_ID', + 'product' => 'ID_DE_TU_PRODUCTO', ]], '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'); +echo "Factura creada: " . $invoice->id; ``` -`downloadPdf()` devuelve bytes crudos de PDF (cadena binaria), no base64. - +**Descargar Archivos de la Factura** ```php -file_put_contents('invoice.pdf', $pdfBytes); -``` +$zipBytes = $facturapi->invoices()->downloadZip('ID_DE_FACTURA'); +$pdfBytes = $facturapi->invoices()->downloadPdf('ID_DE_FACTURA'); // Devuelve bytes crudos +$xmlBytes = $facturapi->invoices()->downloadXml('ID_DE_FACTURA'); -### Enviar por Correo +file_put_contents('factura.pdf', $pdfBytes); +``` +**Enviar por Correo** ```php -$facturapi->Invoices->sendByEmail('INVOICE_ID'); +$facturapi->invoices()->sendByEmail('ID_DE_FACTURA'); ``` -### Catálogos de Comercio Exterior +### 2. Clientes +**Crear un Cliente** ```php -$results = $facturapi->ComercioExteriorCatalogs->searchTariffFractions([ - 'q' => '0101', - 'page' => 0, - 'limit' => 10, +$customer = $facturapi->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', + ], ]); ``` -### Catálogos de Nómina +### 3. Productos +**Crear un Producto** ```php -$deductions = $facturapi->NominaCatalogs->searchDeductions(["q" => "001"]); -$perceptions = $facturapi->NominaCatalogs->searchPerceptions(["q" => "001"]); +$product = $facturapi->products()->create([ + 'product_key' => '4319150114', + 'description' => 'Apple iPhone 8', + 'price' => 345.60, +]); ``` -### Organizaciones: Definir Serie Por Defecto +### 4. Organizaciones y Catálogos +**Definir Serie por Defecto para una Organización** ```php -$result = $facturapi->Organizations->updateDefaultSeries( - 'ORGANIZATION_ID', +$result = $facturapi->organizations()->updateDefaultSeries( + 'ID_DE_LA_ORGANIZACION', [ 'type' => 'I', 'series' => 'A', @@ -175,37 +179,89 @@ $result = $facturapi->Organizations->updateDefaultSeries( ); ``` +**Buscar en Catálogos** +```php +// Comercio Exterior +$results = $facturapi->comercioExteriorCatalogs()->searchTariffFractions([ + 'q' => '0101', + 'page' => 0, + 'limit' => 10, +]); + +// Nómina +$deductions = $facturapi->nominaCatalogs()->searchDeductions(["q" => "001"]); +$perceptions = $facturapi->nominaCatalogs()->searchPerceptions(["q" => "001"]); +``` + +### 5. Webhooks + +**Validar Firma** +Para validar una firma de webhook entrante de manera local (sin llamar a la API), guarda tu secreto de webhook en tus variables de entorno (por ejemplo, `FACTURAPI_WEBHOOK_SECRET`) y pasa el cuerpo raw, el header de la firma y tu secreto al método `validateSignature`: + +```php +$rawBody = file_get_contents('php://input'); +$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? ''; +$webhookSecret = getenv('FACTURAPI_WEBHOOK_SECRET'); + +$result = $facturapi->webhooks()->validateSignature([ + 'body' => $rawBody, + 'signature' => $signature, + 'webhookSecret' => $webhookSecret, +]); + +if (!$result['valid']) { + // Manejar firma inválida +} +``` + +--- + ## 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. +- `getMessage()`: Mensaje de error de la API (si está presente). +- `getStatusCode()`: Código de estado HTTP. +- `getErrorData()`: Payload del error decodificado (con toda la estructura de la API). +- `getRawBody()`: String crudo del body de la respuesta. ```php use Facturapi\Exceptions\FacturapiException; try { - $facturapi->Invoices->create($payload); + $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' => '...'] + $error = $e->getErrorData(); // Shape completo del error de la API + $firstDetail = $error['details'][0] ?? null; // ej. ['path' => 'items.0.quantity', 'message' => '...', 'code' => '...'] } ``` -## Notas de Migración (v4) 🔄 +--- + +## Migrando de v4 a v5 🔄 + +La versión 5 introduce varios "breaking changes" diseñados para hacer el SDK más seguro y moderno: + +1. **DTOs Estrictamente Tipados**: Todos los recursos de la API devuelven Data Transfer Objects (DTOs) e instancian Enums en lugar de arreglos genéricos. + - **Sintaxis v4:** `$customer['legal_name']` + - **Sintaxis v5:** `$customer->legal_name` +2. **Métodos de Recursos**: Los recursos de la API ahora se acceden mediante métodos en lugar de propiedades. + - **Sintaxis v4:** `$facturapi->Customers` + - **Sintaxis v5:** `$facturapi->customers()` +3. **Requiere PHP 8.2+**: Asegúrate de que tu servidor esté ejecutando PHP 8.2 o superior. +4. **Actualización del Cliente HTTP**: La implementación interna del cliente HTTP fue actualizada. El SDK se mantiene compatible con PSR-18 y descubrirá automáticamente los clientes instalados vía `php-http/discovery`. +5. **Características Eliminadas**: + - Se eliminó el soporte para el argumento posicional `apiVersion` en el constructor. + - Se eliminaron los alias de métodos en snake_case. + - `Facturapi\Exceptions\Facturapi_Exception` ha sido eliminado (Usa `Facturapi\Exceptions\FacturapiException`). + +*(Nota: Los proyectos con Composer no requieren cambios en el autoloader. Los proyectos sin Composer deben descargar el `.zip` de la versión 5 y requerir el `autoload.php` empaquetado como de costumbre).* + +Si aún no estás listo para actualizar, puedes fijar tu dependencia a la versión `^4.8`. -- 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 📚 @@ -213,5 +269,5 @@ Documentación completa: [https://docs.facturapi.io](https://docs.facturapi.io) ## Soporte 💬 -- Issues: abre un issue en GitHub -- Email: `contacto@facturapi.io` +- Problemas (Issues): abre un issue en GitHub +- Correo: `contacto@facturapi.io` diff --git a/README.md b/README.md index 5191891..b9c3e65 100644 --- a/README.md +++ b/README.md @@ -9,29 +9,55 @@ Language: English | [Español](./README.es.md) [![Total Downloads](https://img.shields.io/packagist/dt/facturapi/facturapi-php?style=flat-square)](https://packagist.org/packages/facturapi/facturapi-php) [![License](https://img.shields.io/packagist/l/facturapi/facturapi-php?style=flat-square)](https://packagist.org/packages/facturapi/facturapi-php) -## Installation ⚡ +## Compatibility ✅ -```bash -composer require facturapi/facturapi-php +- **PHP**: `>= 8.2` +- **PSR-18 HTTP Client**: Fully compatible (automatically discovered via `php-http/discovery`). +- **Long-Running Processes**: 100% compatible with application servers like **FrankenPHP**, **RoadRunner**, and **Swoole** (e.g., Laravel Octane). The SDK is stateless and does not introduce memory leaks. + +## Developer Experience ✨ + +Version 5 introduces **Strictly Typed DTOs (Data Transfer Objects)**. +Instead of guessing array keys, your IDE will now provide full autocompletion (IntelliSense) for all API responses, making development faster and significantly reducing typos. + +```php +// ❌ v4 (Generic Arrays - No autocompletion) +echo $customer['legal_name']; + +// ✅ v5 (Typed DTOs - Full IDE Autocompletion) +echo $customer->legal_name; ``` -Without Composer (supported workaround): +### Enums Support +The SDK ships with PHP Enums for common catalogs (e.g., `PaymentForm`, `TaxType`). **Using them is completely optional** (you can still pass raw strings like `'01'`), but they are highly recommended. Enums not only power IDE autocompletion and prevent typos, but they also give clear context to AI assistants (LLMs) about what values can be safely injected into the payload. ```php -require_once __DIR__ . '/path/to/facturapi-php/src/Facturapi.php'; +// ✅ Optional but recommended (IDE & AI friendly) +'payment_form' => \Facturapi\PaymentForm::EFECTIVO, + +// 🆗 Also valid +'payment_form' => '01', ``` -Requirements: -- PHP `>=8.2` +## Installation ⚡ -## Backward Compatibility With Older PHP Versions +```bash +composer require facturapi/facturapi-php +``` + +Without Composer: + +1. Download `facturapi-php-standalone.zip` from the [Latest Release](https://github.com/facturapi/facturapi-php/releases/latest). +2. Extract it in your project. +3. Require the bundled autoloader: -Since version **4.0.0**, the SDK requires **PHP >= 8.2**. +```php +require_once __DIR__ . '/path/to/extracted-folder/autoload.php'; +``` -- If your project already runs on PHP 8.2+, no extra changes are needed. -- If your project runs on PHP 8.1 or lower, pin to version **3.7.0** (latest compatible release before PHP 8.2 requirement). +## Client Configuration ⚙️ -## Quick Start 🚀 +Initialize the SDK by passing your API Key. ```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', - ], -]); ``` -## Client Configuration ⚙️ - -Constructor signature: +You can optionally pass a configuration array as the second argument: ```php -new Facturapi(string $apiKey, ?array $config = null) -``` - -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, + 'timeout' => 360, // seconds ]); ``` ### 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: +The SDK works out of the box with its internal Guzzle-based client. If you wish to provide your own PSR-18 compatible client (e.g. to configure proxy settings or custom timeouts): ```php use Facturapi\Facturapi; use GuzzleHttp\Client; -$httpClient = new Client([ - 'timeout' => 420, -]); +$httpClient = new Client(['timeout' => 420]); $facturapi = new Facturapi($apiKey, [ 'httpClient' => $httpClient, ]); ``` -## Common Usage 🧾 +## Framework Integration Examples 📚 -### Create a Product +Check out our official guides for integrating Facturapi with popular PHP frameworks using dependency injection and best practices: -```php -$product = $facturapi->Products->create([ - 'product_key' => '4319150114', - 'description' => 'Apple iPhone 8', - 'price' => 345.60, -]); -``` +- [Laravel Integration](./docs/laravel.md) +- [Symfony Integration](./docs/symfony.md) +- [CodeIgniter 4 Integration](./docs/codeigniter.md) -### Create an Invoice +--- +## Core Usage 🧾 + +### 1. Invoices + +**Create an Invoice** ```php -$invoice = $facturapi->Invoices->create([ +$invoice = $facturapi->invoices()->create([ 'customer' => 'YOUR_CUSTOMER_ID', 'items' => [[ 'quantity' => 1, @@ -124,49 +120,57 @@ $invoice = $facturapi->Invoices->create([ 'folio_number' => '581', 'series' => 'F', ]); -``` -### Download Files - -```php -$zipBytes = $facturapi->Invoices->downloadZip('INVOICE_ID'); -$pdfBytes = $facturapi->Invoices->downloadPdf('INVOICE_ID'); -$xmlBytes = $facturapi->Invoices->downloadXml('INVOICE_ID'); +echo "Created invoice: " . $invoice->id; ``` -`downloadPdf()` returns raw PDF bytes (binary string), not base64. - +**Download Invoice Files** ```php +$zipBytes = $facturapi->invoices()->downloadZip('INVOICE_ID'); +$pdfBytes = $facturapi->invoices()->downloadPdf('INVOICE_ID'); // Returns raw binary string +$xmlBytes = $facturapi->invoices()->downloadXml('INVOICE_ID'); + file_put_contents('invoice.pdf', $pdfBytes); ``` -### Send by Email - +**Send by Email** ```php -$facturapi->Invoices->sendByEmail('INVOICE_ID'); +$facturapi->invoices()->sendByEmail('INVOICE_ID'); ``` -### Comercio Exterior Catalogs +### 2. Customers +**Create a Customer** ```php -$results = $facturapi->ComercioExteriorCatalogs->searchTariffFractions([ - 'q' => '0101', - 'page' => 0, - 'limit' => 10, +$customer = $facturapi->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', + ], ]); ``` -### Payroll Catalogs +### 3. Products +**Create a Product** ```php -$deductions = $facturapi->NominaCatalogs->searchDeductions(["q" => "001"]); -$perceptions = $facturapi->NominaCatalogs->searchPerceptions(["q" => "001"]); +$product = $facturapi->products()->create([ + 'product_key' => '4319150114', + 'description' => 'Apple iPhone 8', + 'price' => 345.60, +]); ``` -### Organizations: Set Default Series +### 4. Organizations & Catalogs +**Set Default Series for an Organization** ```php -$result = $facturapi->Organizations->updateDefaultSeries( +$result = $facturapi->organizations()->updateDefaultSeries( 'ORGANIZATION_ID', [ 'type' => 'I', @@ -175,6 +179,43 @@ $result = $facturapi->Organizations->updateDefaultSeries( ); ``` +**Search Catalogs** +```php +// Comercio Exterior +$results = $facturapi->comercioExteriorCatalogs()->searchTariffFractions([ + 'q' => '0101', + 'page' => 0, + 'limit' => 10, +]); + +// Payroll / Nómina +$deductions = $facturapi->nominaCatalogs()->searchDeductions(["q" => "001"]); +$perceptions = $facturapi->nominaCatalogs()->searchPerceptions(["q" => "001"]); +``` + +### 5. Webhooks + +**Validate Signature** +To validate an incoming webhook signature locally using the SDK, store your webhook secret in your environment variables (e.g. `FACTURAPI_WEBHOOK_SECRET`) and pass the raw body, signature header, and secret to the `validateSignature` method: + +```php +$rawBody = file_get_contents('php://input'); +$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? ''; +$webhookSecret = getenv('FACTURAPI_WEBHOOK_SECRET'); + +$result = $facturapi->webhooks()->validateSignature([ + 'body' => $rawBody, + 'signature' => $signature, + 'webhookSecret' => $webhookSecret, +]); + +if (!$result['valid']) { + // Handle invalid signature +} +``` + +--- + ## Error Handling ⚠️ On non-2xx responses, the SDK throws `Facturapi\Exceptions\FacturapiException`. @@ -189,7 +230,7 @@ The exception includes: use Facturapi\Exceptions\FacturapiException; try { - $facturapi->Invoices->create($payload); + $facturapi->invoices()->create($payload); } catch (FacturapiException $e) { $status = $e->getStatusCode(); $error = $e->getErrorData(); // Full API error shape when body is valid JSON. @@ -197,15 +238,30 @@ try { } ``` -## Migration Notes (v4) 🔄 +--- + +## Migrating from v4 to v5 🔄 + +Version 5 introduces several breaking changes designed to make the SDK safer and more modern: + +1. **Strictly Typed DTOs**: All API resources now return strictly typed Data Transfer Objects (DTOs) and Enums instead of generic associative arrays. + - **v4 syntax:** `$customer['legal_name']` + - **v5 syntax:** `$customer->legal_name` +2. **Resource Methods**: API resources are now accessed via methods instead of properties. + - **v4 syntax:** `$facturapi->Customers` + - **v5 syntax:** `$facturapi->customers()` +3. **PHP 8.2+ Required**: Ensure your server is running PHP 8.2 or higher. +4. **HTTP Client Upgrade**: The internal HTTP client implementation has been updated. The SDK remains PSR-18 compliant and will automatically discover installed clients via `php-http/discovery`. +5. **Removed Features**: + - Removed support for the positional `apiVersion` constructor argument. + - Snake_case method aliases have been removed. + - `Facturapi\Exceptions\Facturapi_Exception` has been removed (Use `Facturapi\Exceptions\FacturapiException`). + +*(Note: Composer projects require no autoloader changes. Non-Composer projects should download the v5 `.zip` release and require the bundled `autoload.php` as usual).* + +If you are not ready to upgrade, you can pin your dependency to `^4.8`. -- 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`. +--- ## Documentation 📚 diff --git a/VERSION.md b/VERSION.md index a20ae58..1ff2824 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1,4 +1,4 @@ -4.8.1 +5.0.0 ## Fixed - Serialize query parameters with the encoding the API documents and the other official SDKs send: lists repeat the key (`status=valid&status=canceled`) instead of using indexes, and `null` values are omitted instead of being sent empty. Associative arrays (`date[gte]=...`) and explicit empty strings (`q=`) keep working. diff --git a/composer.json b/composer.json index a34b01e..df65699 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,9 @@ }, { "name": "Raúl Sánchez" + }, + { + "name": "Arturo Ortega (macarthuror)" } ], "support": { @@ -31,16 +34,45 @@ }, "require": { "php": ">=8.2", - "guzzlehttp/guzzle": "^7.9", - "psr/http-client": "^1.0" + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "php-http/discovery": "^1.19", + "php-http/multipart-stream-builder": "^1.4" }, "require-dev": { - "phpunit/phpunit": "^11.5" + "guzzlehttp/guzzle": "^8.0", + "pestphp/pest": "^5.3", + "phpstan/phpstan": "^2.2", + "phpstan/phpstan-strict-rules": "^2.0", + "laravel/pint": "^1.13", + "rector/rector": "*", + "phpstan/phpstan-deprecation-rules": "^2.0" }, "autoload": { "psr-4": { "Facturapi\\": "src/" } }, - "minimum-stability": "stable" + "minimum-stability": "stable", + "scripts": { + "test": "pest", + "format": "pint", + "format:test": "pint --test", + "analyze": "phpstan analyse --memory-limit=2G --debug", + "lint": [ + "@format:test", + "@analyze" + ] + }, + "config": { + "allow-plugins": { + "php-http/discovery": true, + "pestphp/pest-plugin": true + } + }, + "autoload-dev": { + "psr-4": { + "Facturapi\\Tests\\": "tests/" + } + } } diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..35228c3 --- /dev/null +++ b/composer.lock @@ -0,0 +1,5344 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "5f6447388832780ce1c1d3f1b8536037", + "packages": [ + { + "name": "php-http/discovery", + "version": "1.20.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/discovery.git", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/discovery/zipball/82fe4c73ef3363caed49ff8dd1539ba06044910d", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0|^2.0", + "php": "^7.1 || ^8.0" + }, + "conflict": { + "nyholm/psr7": "<1.0", + "zendframework/zend-diactoros": "*" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "*", + "psr/http-factory-implementation": "*", + "psr/http-message-implementation": "*" + }, + "require-dev": { + "composer/composer": "^1.0.2|^2.0", + "graham-campbell/phpspec-skip-example-extension": "^5.0", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0", + "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3", + "sebastian/comparator": "^3.0.5 || ^4.0.8", + "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1" + }, + "type": "composer-plugin", + "extra": { + "class": "Http\\Discovery\\Composer\\Plugin", + "plugin-optional": true + }, + "autoload": { + "psr-4": { + "Http\\Discovery\\": "src/" + }, + "exclude-from-classmap": [ + "src/Composer/Plugin.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations", + "homepage": "http://php-http.org", + "keywords": [ + "adapter", + "client", + "discovery", + "factory", + "http", + "message", + "psr17", + "psr7" + ], + "support": { + "issues": "https://github.com/php-http/discovery/issues", + "source": "https://github.com/php-http/discovery/tree/1.20.0" + }, + "time": "2024-10-02T11:20:13+00:00" + }, + { + "name": "php-http/multipart-stream-builder", + "version": "1.4.2", + "source": { + "type": "git", + "url": "https://github.com/php-http/multipart-stream-builder.git", + "reference": "10086e6de6f53489cca5ecc45b6f468604d3460e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/multipart-stream-builder/zipball/10086e6de6f53489cca5ecc45b6f468604d3460e", + "reference": "10086e6de6f53489cca5ecc45b6f468604d3460e", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/discovery": "^1.15", + "psr/http-factory-implementation": "^1.0" + }, + "require-dev": { + "nyholm/psr7": "^1.0", + "php-http/message": "^1.5", + "php-http/message-factory": "^1.0.2", + "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Message\\MultipartStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + } + ], + "description": "A builder class that help you create a multipart stream", + "homepage": "http://php-http.org", + "keywords": [ + "factory", + "http", + "message", + "multipart stream", + "stream" + ], + "support": { + "issues": "https://github.com/php-http/multipart-stream-builder/issues", + "source": "https://github.com/php-http/multipart-stream-builder/tree/1.4.2" + }, + "time": "2024-09-04T13:22:54+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + } + ], + "packages-dev": [ + { + "name": "brianium/paratest", + "version": "v7.26.1", + "source": { + "type": "git", + "url": "https://github.com/paratestphp/paratest.git", + "reference": "4660303009de20706b71d2df487bb89fb4d8a4c2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/4660303009de20706b71d2df487bb89fb4d8a4c2", + "reference": "4660303009de20706b71d2df487bb89fb4d8a4c2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-simplexml": "*", + "fidry/cpu-core-counter": "^1.4.1", + "jean85/pretty-package-versions": "^2.1.1", + "php": "~8.4.0 || ~8.5.0 || ~8.6.0", + "phpunit/php-code-coverage": "^14.4.1", + "phpunit/php-file-iterator": "^7.0.2", + "phpunit/php-timer": "^9", + "phpunit/phpunit": "^13.4.1", + "sebastian/environment": "^9.3.3", + "symfony/console": "^7.4.20 || ^8.1.8", + "symfony/process": "^7.4.19 || ^8.1.7" + }, + "require-dev": { + "doctrine/coding-standard": "^14.0.0", + "ext-pcntl": "*", + "ext-pcov": "*", + "ext-posix": "*", + "phpstan/phpstan": "^2.3.1", + "phpstan/phpstan-deprecation-rules": "^2.0.5", + "phpstan/phpstan-phpunit": "^2.1.1", + "phpstan/phpstan-strict-rules": "^2.1.0", + "symfony/filesystem": "^7.4.18 || ^8.1.6" + }, + "bin": [ + "bin/paratest", + "bin/paratest_for_phpstorm" + ], + "type": "library", + "autoload": { + "psr-4": { + "ParaTest\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Scaturro", + "email": "scaturrob@gmail.com", + "role": "Developer" + }, + { + "name": "Filippo Tessarotto", + "email": "zoeslam@gmail.com", + "role": "Developer" + } + ], + "description": "Parallel testing for PHP", + "homepage": "https://github.com/paratestphp/paratest", + "keywords": [ + "concurrent", + "parallel", + "phpunit", + "testing" + ], + "support": { + "issues": "https://github.com/paratestphp/paratest/issues", + "source": "https://github.com/paratestphp/paratest/tree/v7.26.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/Slamdunk", + "type": "github" + }, + { + "url": "https://paypal.me/filippotessarotto", + "type": "paypal" + } + ], + "time": "2026-10-10T20:10:31+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.6", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=14" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^14", + "phpstan/phpstan": "1.4.10 || 2.1.30", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.6" + }, + "time": "2026-02-07T07:09:04+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "d384a7d0d6e67aa497327260ca95023e92dc2032" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/d384a7d0d6e67aa497327260ca95023e92dc2032", + "reference": "d384a7d0d6e67aa497327260ca95023e92dc2032", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0 || ^1.1.2", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "symfony/polyfill-mbstring": "^1.33", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.4.1" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2026-09-29T22:11:39+00:00" + }, + { + "name": "filp/whoops", + "version": "2.18.6", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "7d0a2cde892089bb09372f4366e9b960eb3001a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/7d0a2cde892089bb09372f4366e9b960eb3001a9", + "reference": "7d0a2cde892089bb09372f4366e9b960eb3001a9", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3 || ^10.5.58", + "symfony/var-dumper": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "support": { + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.18.6" + }, + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2026-10-03T17:03:57+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "8.2.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "93939470950a9b11e2e84204166ef5e048c55fe4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/93939470950a9b11e2e84204166ef5e048c55fe4", + "reference": "93939470950a9b11e2e84204166ef5e048c55fe4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^3.0.2", + "guzzlehttp/psr7": "^3.1", + "php": "^7.4 || ^8.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "symfony/polyfill-php80": "^1.25", + "symfony/polyfill-php82": "^1.27" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "4.0.1", + "guzzlehttp/test-server": "^1.0.1", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^9.6.34", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/8.2.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2026-09-06T13:55:09+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "42118e66a53c492effaf92bc357e931985d5c6f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/42118e66a53c492effaf92bc357e931985d5c6f9", + "reference": "42118e66a53c492effaf92bc357e931985d5c6f9", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^9.6.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2026-08-24T10:00:26+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "a3059ba1a84c9139c4ae03cf0f45bea276c97c74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/a3059ba1a84c9139c4ae03cf0f45bea276c97c74", + "reference": "a3059ba1a84c9139c4ae03cf0f45bea276c97c74", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/http-factory": "^1.1", + "psr/http-message": "^2.0", + "symfony/polyfill-php80": "^1.25", + "symfony/polyfill-php82": "^1.27" + }, + "provide": { + "psr/http-factory-implementation": "1.1", + "psr/http-message-implementation": "2.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "1.1.0", + "jshttp/mime-db": "1.54.0.1", + "php-http/psr7-integration-tests": "^1.5.1", + "phpunit/phpunit": "^9.6.34" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/3.1.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2026-08-24T11:02:13+00:00" + }, + { + "name": "jean85/pretty-package-versions", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.1.0", + "php": "^7.4|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "jean85/composer-provided-replaced-stub-package": "^1.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^7.5|^8.5|^9.6", + "rector/rector": "^2.0", + "vimeo/psalm": "^4.3 || ^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A library to get pretty versions strings of installed dependencies", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "support": { + "issues": "https://github.com/Jean85/pretty-package-versions/issues", + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1" + }, + "time": "2025-03-19T14:43:43+00:00" + }, + { + "name": "laravel/pint", + "version": "v1.32.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/pint.git", + "reference": "006bd7825eb01daf92959d060b89cd9514fe4253" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pint/zipball/006bd7825eb01daf92959d060b89cd9514fe4253", + "reference": "006bd7825eb01daf92959d060b89cd9514fe4253", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "ext-xml": "*", + "php": "^8.3.0" + }, + "require-dev": { + "composer/semver": "^3.4.4", + "friendsofphp/php-cs-fixer": "^3.95.25", + "illuminate/view": "^13.31.0", + "larastan/larastan": "^3.11.0", + "laravel-zero/framework": "^13.0.0", + "laravel/agent-detector": "^2.0.2", + "laravel/prompts": "^0.3.24", + "mockery/mockery": "^1.6.15", + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest": "^4.7.8" + }, + "bin": [ + "builds/pint" + ], + "type": "project", + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Seeders\\": "database/seeders/", + "Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "An opinionated code formatter for PHP.", + "homepage": "https://laravel.com", + "keywords": [ + "dev", + "format", + "formatter", + "lint", + "linter", + "php" + ], + "support": { + "issues": "https://github.com/laravel/pint/issues", + "source": "https://github.com/laravel/pint" + }, + "time": "2026-09-10T10:47:55+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.14.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.14.0" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + } + ], + "time": "2026-08-11T10:17:44+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.9.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/9e33da9553fe7786f0962b35f4e4ecf01be89def", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.9.0" + }, + "time": "2026-09-13T18:51:52+00:00" + }, + { + "name": "nunomaduro/collision", + "version": "v8.9.5", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/collision.git", + "reference": "fb53eacd509a1d303858e2d20cfebf2d630254ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/fb53eacd509a1d303858e2d20cfebf2d630254ec", + "reference": "fb53eacd509a1d303858e2d20cfebf2d630254ec", + "shasum": "" + }, + "require": { + "filp/whoops": "^2.18.4", + "nunomaduro/termwind": "^2.4.0", + "php": "^8.2.0", + "symfony/console": "^7.4.14 || ^8.1.1" + }, + "conflict": { + "laravel/framework": "<11.48.0 || >=14.0.0", + "phpunit/phpunit": "<11.5.50 || >=14.0.0" + }, + "require-dev": { + "brianium/paratest": "^7.8.5", + "larastan/larastan": "^3.10.0", + "laravel/framework": "^11.48.0 || ^12.56.0 || ^13.20.0", + "laravel/pint": "^1.29.3", + "orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.3.5", + "pestphp/pest": "^3.8.5 || ^4.7.5 || ^5.0.0", + "sebastian/environment": "^7.2.1 || ^8.1.2 || ^9.3.2" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" + ] + }, + "branch-alias": { + "dev-8.x": "8.x-dev" + } + }, + "autoload": { + "files": [ + "./src/Adapters/Phpunit/Autoload.php" + ], + "psr-4": { + "NunoMaduro\\Collision\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Cli error handling for console/command-line PHP applications.", + "keywords": [ + "artisan", + "cli", + "command-line", + "console", + "dev", + "error", + "handling", + "laravel", + "laravel-zero", + "php", + "symfony" + ], + "support": { + "issues": "https://github.com/nunomaduro/collision/issues", + "source": "https://github.com/nunomaduro/collision" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2026-07-15T19:09:14+00:00" + }, + { + "name": "nunomaduro/termwind", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/termwind.git", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/712a31b768f5daea284c2169a7d227031001b9a8", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^8.2", + "symfony/console": "^7.4.4 || ^8.0.4" + }, + "require-dev": { + "illuminate/console": "^11.47.0", + "laravel/pint": "^1.27.1", + "mockery/mockery": "^1.6.12", + "pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.3.2", + "phpstan/phpstan": "^1.12.32", + "phpstan/phpstan-strict-rules": "^1.6.2", + "symfony/var-dumper": "^7.3.5 || ^8.0.4", + "thecodingmachine/phpstan-strict-rules": "^1.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Termwind\\Laravel\\TermwindServiceProvider" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Functions.php" + ], + "psr-4": { + "Termwind\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "It's like Tailwind CSS, but for the console.", + "keywords": [ + "cli", + "console", + "css", + "package", + "php", + "style" + ], + "support": { + "issues": "https://github.com/nunomaduro/termwind/issues", + "source": "https://github.com/nunomaduro/termwind/tree/v2.4.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://github.com/xiCO2k", + "type": "github" + } + ], + "time": "2026-02-16T23:10:27+00:00" + }, + { + "name": "pestphp/pest", + "version": "v5.3.1", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest.git", + "reference": "83baa5de2f64967d48326ab93594e45d300bd105" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest/zipball/83baa5de2f64967d48326ab93594e45d300bd105", + "reference": "83baa5de2f64967d48326ab93594e45d300bd105", + "shasum": "" + }, + "require": { + "brianium/paratest": "^7.26.0", + "nunomaduro/collision": "^8.9.5", + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest-plugin": "^5.0.0", + "pestphp/pest-plugin-arch": "^5.0.0", + "pestphp/pest-plugin-mutate": "^5.0.2", + "pestphp/pest-plugin-profanity": "^5.0.1", + "php": "^8.4", + "phpunit/phpunit": "^13.4.1", + "symfony/process": "^8.1.7" + }, + "conflict": { + "filp/whoops": "<2.18.3", + "laravel/boost": "<2.6.0", + "phpunit/phpunit": ">13.4.1", + "sebastian/exporter": "<7.0.0", + "webmozart/assert": "<1.11.0" + }, + "require-dev": { + "pestphp/pest-dev-tools": "^5.0.0", + "pestphp/pest-plugin-browser": "^5.1.2", + "pestphp/pest-plugin-phpstan": "^5.2.2", + "pestphp/pest-plugin-rector": "^5.0.5", + "pestphp/pest-plugin-type-coverage": "^5.0.3", + "psy/psysh": "^0.12.24" + }, + "bin": [ + "bin/pest" + ], + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Mutate\\Plugins\\Mutate", + "Pest\\Plugins\\Configuration", + "Pest\\Plugins\\Bail", + "Pest\\Plugins\\Cache", + "Pest\\Plugins\\Coverage", + "Pest\\Plugins\\Init", + "Pest\\Plugins\\Environment", + "Pest\\Plugins\\Help", + "Pest\\Plugins\\Memory", + "Pest\\Plugins\\Only", + "Pest\\Plugins\\Printer", + "Pest\\Plugins\\ProcessIsolation", + "Pest\\Plugins\\Profile", + "Pest\\Plugins\\Retry", + "Pest\\Plugins\\Snapshot", + "Pest\\Plugins\\Verbose", + "Pest\\Plugins\\Version", + "Pest\\Plugins\\Shard", + "Pest\\Plugins\\Tia", + "Pest\\Plugins\\Parallel" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "files": [ + "src/Functions.php", + "src/Pest.php" + ], + "psr-4": { + "Pest\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "The elegant PHP Testing Framework.", + "keywords": [ + "framework", + "pest", + "php", + "test", + "testing", + "unit" + ], + "support": { + "issues": "https://github.com/pestphp/pest/issues", + "source": "https://github.com/pestphp/pest/tree/v5.3.1" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2026-10-07T20:11:43+00:00" + }, + { + "name": "pestphp/pest-plugin", + "version": "v5.0.0", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin.git", + "reference": "87283f41aafa2561b7618be53eab00f2d06f4140" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin/zipball/87283f41aafa2561b7618be53eab00f2d06f4140", + "reference": "87283f41aafa2561b7618be53eab00f2d06f4140", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0.0", + "composer-runtime-api": "^2.2.2", + "php": "^8.4" + }, + "conflict": { + "pestphp/pest": "<5.0.0" + }, + "require-dev": { + "composer/composer": "^2.10.2", + "pestphp/pest": "^5.0.0", + "pestphp/pest-dev-tools": "^5.0.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Pest\\Plugin\\Manager" + }, + "autoload": { + "psr-4": { + "Pest\\Plugin\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Pest plugin manager", + "keywords": [ + "framework", + "manager", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin/tree/v5.0.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2026-07-18T17:10:45+00:00" + }, + { + "name": "pestphp/pest-plugin-arch", + "version": "v5.0.0", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-arch.git", + "reference": "244465d5dc9ea9fb5ac5c9badb7eb47d1594e4c1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/244465d5dc9ea9fb5ac5c9badb7eb47d1594e4c1", + "reference": "244465d5dc9ea9fb5ac5c9badb7eb47d1594e4c1", + "shasum": "" + }, + "require": { + "pestphp/pest-plugin": "^5.0.0", + "php": "^8.4", + "ta-tikoma/phpunit-architecture-test": "^0.8.7" + }, + "conflict": { + "pestphp/pest": "<5.0.0" + }, + "require-dev": { + "pestphp/pest": "^5.0.0", + "pestphp/pest-dev-tools": "^5.0.0" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Arch\\Plugin" + ] + } + }, + "autoload": { + "files": [ + "src/Autoload.php" + ], + "psr-4": { + "Pest\\Arch\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Arch plugin for Pest PHP.", + "keywords": [ + "arch", + "architecture", + "framework", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-arch/tree/v5.0.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2026-07-21T07:48:55+00:00" + }, + { + "name": "pestphp/pest-plugin-mutate", + "version": "v5.0.2", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-mutate.git", + "reference": "d0c87110416b699413cc073b8dcc92e440d81931" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-mutate/zipball/d0c87110416b699413cc073b8dcc92e440d81931", + "reference": "d0c87110416b699413cc073b8dcc92e440d81931", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.8.0", + "pestphp/pest-plugin": "^5.0.0", + "php": "^8.4", + "phpunit/php-code-coverage": "^14.3.0", + "psr/simple-cache": "^3.0.0" + }, + "conflict": { + "pestphp/pest": "<5.0.0" + }, + "require-dev": { + "pestphp/pest": "^5.0.4", + "pestphp/pest-dev-tools": "^5.0.0", + "pestphp/pest-plugin-type-coverage": "^5.0.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Pest\\Mutate\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + }, + { + "name": "Sandro Gehri", + "email": "sandrogehri@gmail.com" + } + ], + "description": "Mutates your code to find untested cases", + "keywords": [ + "framework", + "mutate", + "mutation", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-mutate/tree/v5.0.2" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/gehrisandro", + "type": "github" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2026-08-10T11:01:27+00:00" + }, + { + "name": "pestphp/pest-plugin-profanity", + "version": "v5.0.1", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-profanity.git", + "reference": "2b5fc73ce561b835ba5bf9596c97a84333af2eb8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-profanity/zipball/2b5fc73ce561b835ba5bf9596c97a84333af2eb8", + "reference": "2b5fc73ce561b835ba5bf9596c97a84333af2eb8", + "shasum": "" + }, + "require": { + "pestphp/pest-plugin": "^5.0.0", + "php": "^8.4" + }, + "conflict": { + "pestphp/pest": "<5.0.0", + "phpunit/phpunit": "<13.4.1" + }, + "require-dev": { + "faissaloux/pest-plugin-inside": "^1.11", + "pestphp/pest": "^5.0.0", + "pestphp/pest-dev-tools": "^5.0.0" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Profanity\\Plugin" + ] + } + }, + "autoload": { + "psr-4": { + "Pest\\Profanity\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Pest Profanity Plugin", + "keywords": [ + "framework", + "pest", + "php", + "plugin", + "profanity", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-profanity/tree/v5.0.1" + }, + "time": "2026-10-07T19:41:52+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "ee15a5a2022c8383906b225055e9496a6a5e6e3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/ee15a5a2022c8383906b225055e9496a6a5e6e3b", + "reference": "ee15a5a2022c8383906b225055e9496a6a5e6e3b", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.8", + "phpunit/phpunit": "^9.5", + "vimeo/psalm": "^4.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "https://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.2.1" + }, + "time": "2025-11-10T21:21:55+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/7bae67520aa9f5ecc506d646810bd40d9da54582", + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1", + "ext-filter": "*", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^2.0", + "phpstan/phpdoc-parser": "^2.0", + "webmozart/assert": "^1.9.1 || ^2" + }, + "require-dev": { + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26", + "shipmonk/dead-code-detector": "^0.5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.3" + }, + "time": "2026-03-18T20:49:53+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "dbe2133bb23e2e33a6834f4ed417f24a1b167769" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/dbe2133bb23e2e33a6834f4ed417f24a1b167769", + "reference": "dbe2133bb23e2e33a6834f4ed417f24a1b167769", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev", + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/2.1.0" + }, + "time": "2026-09-27T19:32:54+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.3.6", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "1427af4647235b4a73b13940b1f1258d3584f36e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1427af4647235b4a73b13940b1f1258d3584f36e", + "reference": "1427af4647235b4a73b13940b1f1258d3584f36e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.6" + }, + "time": "2026-09-27T20:07:45+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "2.3.1", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/6155239b488e95d26d1ebd95c6b4bb294d7f09db", + "reference": "6155239b488e95d26d1ebd95c6b4bb294d7f09db", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ondřej Mirtes" + }, + { + "name": "Markus Staab" + }, + { + "name": "Vincent Langlet" + } + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2026-10-08T21:09:11+00:00" + }, + { + "name": "phpstan/phpstan-deprecation-rules", + "version": "2.0.5", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", + "reference": "67bedd65c24bc72840afc45aed48b1059dd44bec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/67bedd65c24bc72840afc45aed48b1059dd44bec", + "reference": "67bedd65c24bc72840afc45aed48b1059dd44bec", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.1.39" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "shipmonk/name-collision-detector": "^2.1" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues", + "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/2.0.5" + }, + "time": "2026-07-22T06:50:43+00:00" + }, + { + "name": "phpstan/phpstan-strict-rules", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-strict-rules.git", + "reference": "24ec51589f896f048d0d36de373d993b5cf71786" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/24ec51589f896f048d0d36de373d993b5cf71786", + "reference": "24ec51589f896f048d0d36de373d993b5cf71786", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.3" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "shipmonk/name-collision-detector": "^2.1" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Extra strict and opinionated rules for PHPStan", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.1.0" + }, + "time": "2026-10-01T21:30:37+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "14.4.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "f1e54f06b6b4cea73cd6104ba3c0cfa0efa6d73f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f1e54f06b6b4cea73cd6104ba3c0cfa0efa6d73f", + "reference": "f1e54f06b6b4cea73cd6104ba3c0cfa0efa6d73f", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.9.0", + "php": ">=8.4", + "phpunit/php-text-template": "^6.0.1", + "sebastian/complexity": "^6.0", + "sebastian/environment": "^9.3.3", + "sebastian/git-state": "^1.0", + "sebastian/lines-of-code": "^5.0.2", + "sebastian/version": "^7.0", + "theseer/tokenizer": "^2.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.6" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "14.4.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/14.4.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2026-10-03T14:05:38+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "9bb4e6c58b62c1e043be995c66abec7c97307aae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/9bb4e6c58b62c1e043be995c66abec7c97307aae", + "reference": "9bb4e6c58b62c1e043be995c66abec7c97307aae", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-08-25T14:47:43+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88", + "reference": "42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^13.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-invoker", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:34:47+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "97107691cb2b6c743b5e0c44be19e983cb337a02" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/97107691cb2b6c743b5e0c44be19e983cb337a02", + "reference": "97107691cb2b6c743b5e0c44be19e983cb337a02", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-text-template", + "type": "tidelift" + } + ], + "time": "2026-10-03T04:54:24+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "9.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "a0e12065831f6ab0d83120dc61513eb8d9a966f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/a0e12065831f6ab0d83120dc61513eb8d9a966f6", + "reference": "a0e12065831f6ab0d83120dc61513eb8d9a966f6", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/9.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-timer", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:37:53+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "13.4.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "ccde40327c501eebff192614b0dcf11806418901" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ccde40327c501eebff192614b0dcf11806418901", + "reference": "ccde40327c501eebff192614b0dcf11806418901", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-filter": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.14.0", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.4.1", + "phpunit/php-code-coverage": "^14.4.1", + "phpunit/php-file-iterator": "^7.0.2", + "phpunit/php-invoker": "^7.0.0", + "phpunit/php-text-template": "^6.0.1", + "phpunit/php-timer": "^9.0.0", + "sebastian/cli-parser": "^5.0.1", + "sebastian/comparator": "^8.4", + "sebastian/diff": "^9.0.1", + "sebastian/environment": "^9.3.3", + "sebastian/exporter": "^8.2.1", + "sebastian/file-filter": "^1.0", + "sebastian/git-state": "^1.0", + "sebastian/global-state": "^9.0.1", + "sebastian/object-enumerator": "^8.1.0", + "sebastian/recursion-context": "^8.0.1", + "sebastian/type": "^7.0.2", + "sebastian/version": "^7.0.1", + "staabm/side-effects-detector": "^1.0.5" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "13.4-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/13.4.1" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsoring.html", + "type": "other" + } + ], + "time": "2026-10-05T05:51:21+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "rector/rector", + "version": "2.7.0", + "source": { + "type": "git", + "url": "https://github.com/rectorphp/rector.git", + "reference": "4d6a39b7fff302cac0ce90cd959a53c2f6a32776" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/4d6a39b7fff302cac0ce90cd959a53c2f6a32776", + "reference": "4d6a39b7fff302cac0ce90cd959a53c2f6a32776", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0", + "phpstan/phpstan": "^2.3.0" + }, + "conflict": { + "rector/rector-doctrine": "*", + "rector/rector-downgrade-php": "*", + "rector/rector-phpunit": "*", + "rector/rector-symfony": "*" + }, + "bin": [ + "bin/rector" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Instant Upgrade and Automated Refactoring of any PHP code", + "homepage": "https://getrector.com/", + "keywords": [ + "automation", + "dev", + "migration", + "refactoring" + ], + "support": { + "issues": "https://github.com/rectorphp/rector/issues", + "source": "https://github.com/rectorphp/rector/tree/2.7.0" + }, + "funding": [ + { + "url": "https://github.com/tomasvotruba", + "type": "github" + } + ], + "time": "2026-10-06T08:46:45+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "eeb759ad3146b7096fb59c3195d39e071cd409e3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/eeb759ad3146b7096fb59c3195d39e071cd409e3", + "reference": "eeb759ad3146b7096fb59c3195d39e071cd409e3", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.2.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" + } + ], + "time": "2026-08-01T04:27:14+00:00" + }, + { + "name": "sebastian/comparator", + "version": "8.4.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "3b070e608146cba00fd6fd1f0ffba89e5a8897fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/3b070e608146cba00fd6fd1f0ffba89e5a8897fb", + "reference": "3b070e608146cba00fd6fd1f0ffba89e5a8897fb", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.4", + "sebastian/diff": "^9.0", + "sebastian/exporter": "^8.2" + }, + "require-dev": { + "phpunit/phpunit": "^13.3" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/8.4.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-08-07T07:23:13+00:00" + }, + { + "name": "sebastian/complexity", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "c5651c795c98093480df79350cb050813fc7a2f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/c5651c795c98093480df79350cb050813fc7a2f3", + "reference": "c5651c795c98093480df79350cb050813fc7a2f3", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/complexity", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:41:32+00:00" + }, + { + "name": "sebastian/diff", + "version": "9.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "a2df6626c1baf31d5a88674882a3072f151b5a26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/a2df6626c1baf31d5a88674882a3072f151b5a26", + "reference": "a2df6626c1baf31d5a88674882a3072f151b5a26", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.1", + "symfony/process": "^7.4.17" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/9.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/diff", + "type": "tidelift" + } + ], + "time": "2026-08-25T15:38:55+00:00" + }, + { + "name": "sebastian/environment", + "version": "9.3.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "d4dd10267a11055066a2a78ae04583d8ddd4f595" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/d4dd10267a11055066a2a78ae04583d8ddd4f595", + "reference": "d4dd10267a11055066a2a78ae04583d8ddd4f595", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.6" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/9.3.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2026-10-01T12:37:56+00:00" + }, + { + "name": "sebastian/exporter", + "version": "8.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "24a3b69bba4a12ab615fca9d34680c5598d9ab7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/24a3b69bba4a12ab615fca9d34680c5598d9ab7a", + "reference": "24a3b69bba4a12ab615fca9d34680c5598d9ab7a", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.4", + "sebastian/recursion-context": "^8.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^13.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/8.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2026-08-07T07:22:06+00:00" + }, + { + "name": "sebastian/file-filter", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/file-filter.git", + "reference": "33a26f394330f6faa7684bb9cc73afb7727aae93" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/file-filter/zipball/33a26f394330f6faa7684bb9cc73afb7727aae93", + "reference": "33a26f394330f6faa7684bb9cc73afb7727aae93", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for filtering files", + "homepage": "https://github.com/sebastianbergmann/file-filter", + "support": { + "issues": "https://github.com/sebastianbergmann/file-filter/issues", + "security": "https://github.com/sebastianbergmann/file-filter/security/policy", + "source": "https://github.com/sebastianbergmann/file-filter/tree/1.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/file-filter", + "type": "tidelift" + } + ], + "time": "2026-04-22T07:20:04+00:00" + }, + { + "name": "sebastian/git-state", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/git-state.git", + "reference": "792a952e0eba55b6960a48aeceb9f371aad1f76b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/git-state/zipball/792a952e0eba55b6960a48aeceb9f371aad1f76b", + "reference": "792a952e0eba55b6960a48aeceb9f371aad1f76b", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for describing the state of a Git checkout", + "homepage": "https://github.com/sebastianbergmann/git-state", + "support": { + "issues": "https://github.com/sebastianbergmann/git-state/issues", + "security": "https://github.com/sebastianbergmann/git-state/security/policy", + "source": "https://github.com/sebastianbergmann/git-state/tree/1.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/git-state", + "type": "tidelift" + } + ], + "time": "2026-03-21T12:54:28+00:00" + }, + { + "name": "sebastian/global-state", + "version": "9.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "ba68ba79da690cf7eddefd3ce5b78b20b9ba9945" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ba68ba79da690cf7eddefd3ce5b78b20b9ba9945", + "reference": "ba68ba79da690cf7eddefd3ce5b78b20b9ba9945", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "sebastian/object-reflector": "^6.0", + "sebastian/recursion-context": "^8.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^13.1.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/9.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2026-06-01T15:11:33+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d1b6f8fce682505dbd048977f1abedf1b8ad3ff8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d1b6f8fce682505dbd048977f1abedf1b8ad3ff8", + "reference": "d1b6f8fce682505dbd048977f1abedf1b8ad3ff8", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.8.0", + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.2.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/5.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/lines-of-code", + "type": "tidelift" + } + ], + "time": "2026-07-09T08:42:34+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "8.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "511064ecde82bd747e2ba2fab3dda8d977b59576" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/511064ecde82bd747e2ba2fab3dda8d977b59576", + "reference": "511064ecde82bd747e2ba2fab3dda8d977b59576", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "sebastian/recursion-context": "^8.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/8.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/object-enumerator", + "type": "tidelift" + } + ], + "time": "2026-08-13T07:05:05+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "6.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "f71bbcdc4f95456b4622810bec64eb06372e25b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/f71bbcdc4f95456b4622810bec64eb06372e25b2", + "reference": "f71bbcdc4f95456b4622810bec64eb06372e25b2", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/6.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/object-reflector", + "type": "tidelift" + } + ], + "time": "2026-08-13T06:34:36+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "8.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "32dba72f2b4642d6a93db22d6c0a9280ff2e3ca0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/32dba72f2b4642d6a93db22d6c0a9280ff2e3ca0", + "reference": "32dba72f2b4642d6a93db22d6c0a9280ff2e3ca0", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.2.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/8.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2026-08-03T05:58:12+00:00" + }, + { + "name": "sebastian/type", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "bd1df467864cb95140414059a535b2d906173fcf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/bd1df467864cb95140414059a535b2d906173fcf", + "reference": "bd1df467864cb95140414059a535b2d906173fcf", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2026-08-10T08:00:57+00:00" + }, + { + "name": "sebastian/version", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "f1598cf1d8d1ca4c0f194a52f2fe72a559a7b0b1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/f1598cf1d8d1ca4c0f194a52f2fe72a559a7b0b1", + "reference": "f1598cf1d8d1ca4c0f194a52f2fe72a559a7b0b1", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/version", + "type": "tidelift" + } + ], + "time": "2026-10-04T05:51:14+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "symfony/console", + "version": "v8.1.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "4b81146e3ee248ea969186f85499b54fbc78db08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/4b81146e3ee248ea969186f85499b54fbc78db08", + "reference": "4b81146e3ee248ea969186f85499b54fbc78db08", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php85": "^1.32", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.4.6|^8.0.6" + }, + "conflict": { + "symfony/dependency-injection": "<8.1", + "symfony/event-dispatcher": "<8.1" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^8.1", + "symfony/event-dispatcher": "^8.1", + "symfony/filesystem": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/lock": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v8.1.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-18T16:00:27+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "symfony/finder", + "version": "v8.1.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "bd4fed193a3132c91c5c93595cbfc78988915822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/bd4fed193a3132c91c5c93595cbfc78988915822", + "reference": "bd4fed193a3132c91c5c93595cbfc78988915822", + "shasum": "" + }, + "require": { + "php": ">=8.4.1" + }, + "require-dev": { + "symfony/filesystem": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v8.1.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-26T06:33:49+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "5cac8f4db97208823aa353f59a01faad2da52c75" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/5cac8f4db97208823aa353f59a01faad2da52c75", + "reference": "5cac8f4db97208823aa353f59a01faad2da52c75", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-25T15:46:47+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "ebd57ccc7edb93d632e184efbe7693d2517bab3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/ebd57ccc7edb93d632e184efbe7693d2517bab3b", + "reference": "ebd57ccc7edb93d632e184efbe7693d2517bab3b", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-23T22:02:09+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "4d5d331bf731e0aedbc6b2f5e61135b7b7996a72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/4d5d331bf731e0aedbc6b2f5e61135b7b7996a72", + "reference": "4d5d331bf731e0aedbc6b2f5e61135b7b7996a72", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-24T15:19:04+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74", + "reference": "9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-25T15:50:05+00:00" + }, + { + "name": "symfony/polyfill-php82", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php82.git", + "reference": "843bbe8b2a7934307b749751eede0f111f5d1cfc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/843bbe8b2a7934307b749751eede0f111f5d1cfc", + "reference": "843bbe8b2a7934307b749751eede0f111f5d1cfc", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php82\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php82/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-25T15:50:05+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "86a42cbcc07adc331481a5ab310a4024bfc227f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/86a42cbcc07adc331481a5ab310a4024bfc227f6", + "reference": "86a42cbcc07adc331481a5ab310a4024bfc227f6", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-25T15:46:47+00:00" + }, + { + "name": "symfony/process", + "version": "v8.1.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "10823b09358e690df4ff943e24e8492bb1019fc3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/10823b09358e690df4ff943e24e8492bb1019fc3", + "reference": "10823b09358e690df4ff943e24e8492bb1019fc3", + "shasum": "" + }, + "require": { + "php": ">=8.4.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v8.1.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-02T12:40:29+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.7.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.7.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-27T15:39:01+00:00" + }, + { + "name": "symfony/string", + "version": "v8.1.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "d950140b5f56f31901e5b7a0c04ffc3a3deb943c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/d950140b5f56f31901e5b7a0c04ffc3a3deb943c", + "reference": "d950140b5f56f31901e5b7a0c04ffc3a3deb943c", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v8.1.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-11T14:51:16+00:00" + }, + { + "name": "ta-tikoma/phpunit-architecture-test", + "version": "0.8.7", + "source": { + "type": "git", + "url": "https://github.com/ta-tikoma/phpunit-architecture-test.git", + "reference": "1248f3f506ca9641d4f68cebcd538fa489754db8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ta-tikoma/phpunit-architecture-test/zipball/1248f3f506ca9641d4f68cebcd538fa489754db8", + "reference": "1248f3f506ca9641d4f68cebcd538fa489754db8", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18.0 || ^5.0.0", + "php": "^8.1.0", + "phpdocumentor/reflection-docblock": "^5.3.0 || ^6.0.0", + "phpunit/phpunit": "^10.5.5 || ^11.0.0 || ^12.0.0 || ^13.0.0", + "symfony/finder": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "require-dev": { + "laravel/pint": "^1.13.7", + "phpstan/phpstan": "^1.10.52" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPUnit\\Architecture\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ni Shi", + "email": "futik0ma011@gmail.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Methods for testing application architecture", + "keywords": [ + "architecture", + "phpunit", + "stucture", + "test", + "testing" + ], + "support": { + "issues": "https://github.com/ta-tikoma/phpunit-architecture-test/issues", + "source": "https://github.com/ta-tikoma/phpunit-architecture-test/tree/0.8.7" + }, + "time": "2026-02-17T17:25:14+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^8.1" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-12-08T11:19:18+00:00" + }, + { + "name": "webmozart/assert", + "version": "2.5.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "8a163706bbcac613733dba03f51e13b5eb1bb819" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/8a163706bbcac613733dba03f51e13b5eb1bb819", + "reference": "8a163706bbcac613733dba03f51e13b5eb1bb819", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-date": "*", + "ext-filter": "*", + "php": "^8.2" + }, + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" + }, + "type": "library", + "extra": { + "psalm": { + "pluginClass": "Webmozart\\Assert\\PsalmPlugin" + }, + "branch-alias": { + "dev-master": "2.0-dev", + "dev-feature/2-0": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/2.5.0" + }, + "time": "2026-10-05T23:14:19+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=8.2" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/docs/codeigniter.md b/docs/codeigniter.md new file mode 100644 index 0000000..39126a7 --- /dev/null +++ b/docs/codeigniter.md @@ -0,0 +1,105 @@ +# Integration with CodeIgniter 4 + +This document shows how to integrate the Facturapi SDK into a CodeIgniter 4 project. + +## 1. Environment Variables Configuration + +Add your Facturapi API key to your `.env` file: + +```env +FACTURAPI_KEY=sk_test_your_api_key +FACTURAPI_WEBHOOK_SECRET=your_webhook_secret +``` + +## 2. Services Configuration + +To have a single shared instance of the SDK throughout your application, configure it in `app/Config/Services.php`: + +```php +namespace Config; + +use CodeIgniter\Config\BaseService; +use Facturapi\Facturapi; + +class Services extends BaseService +{ + // ... + + public static function facturapi($getShared = true) + { + if ($getShared) { + return static::getSharedInstance('facturapi'); + } + + $apiKey = env('FACTURAPI_KEY'); + return new Facturapi($apiKey); + } +} +``` + +## 3. Usage in a Controller + +You can access the service instance we just created in any controller using the `Config\Services` class: + +```php +namespace App\Controllers; + +use Config\Services; + +class InvoiceController extends BaseController +{ + public function index() + { + $facturapi = Services::facturapi(); + + // Usage example: List customers + $customers = $facturapi->customers()->all(); + + return $this->response->setJSON($customers); + } +} +``` + +## 4. Webhook Validation + +You can validate incoming webhooks locally using the `validateSignature` method: + +```php +namespace App\Controllers; + +use Config\Services; + +class WebhookController extends BaseController +{ + public function handle() + { + $facturapi = Services::facturapi(); + + $isValid = $facturapi->webhooks()->validateSignature([ + 'body' => $this->request->getBody(), + 'signature' => $this->request->getHeaderLine('x-signature'), + 'webhookSecret' => env('FACTURAPI_WEBHOOK_SECRET'), + ]); + + if (!$isValid['valid']) { + return $this->response->setStatusCode(401)->setJSON(['error' => 'Invalid signature']); + } + + // Process your webhook... + + return $this->response->setJSON(['status' => 'ok']); + } +} +``` + +## 5. Long-Running Processes (FrankenPHP, RoadRunner, Swoole) + +If you run CodeIgniter via a long-lived process (like FrankenPHP or Swoole), returning `static::getSharedInstance('facturapi')` is perfectly safe if you use a single, global API key. + +However, if your application is **multi-tenant** and requires a different API key per request, you should instantiate the SDK on-demand rather than as a shared service, ensuring the correct key is loaded for the current request: + +```php +// Retrieve a fresh, non-shared instance for the current tenant +$facturapi = Services::facturapi(false); +``` +(Be sure to modify your Service to accept or look up the tenant's API key when `$getShared` is `false`). diff --git a/docs/laravel.md b/docs/laravel.md new file mode 100644 index 0000000..3fdfdce --- /dev/null +++ b/docs/laravel.md @@ -0,0 +1,122 @@ +# Integration with Laravel + +This document shows how to integrate the Facturapi SDK into a Laravel project. + +## 1. Environment Variables Configuration + +Add your Facturapi API key to your `.env` file: + +```env +FACTURAPI_KEY=sk_test_your_api_key +FACTURAPI_WEBHOOK_SECRET=your_webhook_secret +``` + +## 2. Services Configuration + +Add the Facturapi configuration in the `config/services.php` file: + +```php +// config/services.php +return [ + // ... + 'facturapi' => [ + 'key' => env('FACTURAPI_KEY'), + 'webhook_secret' => env('FACTURAPI_WEBHOOK_SECRET'), + ], +]; +``` + +## 3. Service Container Registration + +Register the `Facturapi\Facturapi` class as a Singleton in `app/Providers/AppServiceProvider.php`: + +```php +namespace App\Providers; + +use Illuminate\Support\ServiceProvider; +use Facturapi\Facturapi; + +class AppServiceProvider extends ServiceProvider +{ + public function register(): void + { + $this->app->singleton(Facturapi::class, function ($app) { + return new Facturapi(config('services.facturapi.key')); + }); + } + + public function boot(): void + { + // + } +} +``` + +## 4. Usage in a Controller + +You can now inject the Facturapi instance directly into your controllers: + +```php +namespace App\Http\Controllers; + +use Facturapi\Facturapi; +use Illuminate\Http\JsonResponse; + +class InvoiceController extends Controller +{ + public function index(Facturapi $facturapi): JsonResponse + { + // Usage example: List customers + $customers = $facturapi->customers()->all(); + + return response()->json($customers); + } +} +``` + +## 5. Webhook Validation + +You can easily validate incoming webhooks locally (without an API call) by passing the raw request body, the signature header, and your secret to the `validateSignature` method: + +```php +namespace App\Http\Controllers; + +use Facturapi\Facturapi; +use Illuminate\Http\Request; +use Illuminate\Http\JsonResponse; + +class WebhookController extends Controller +{ + public function handle(Request $request, Facturapi $facturapi): JsonResponse + { + $isValid = $facturapi->webhooks()->validateSignature([ + 'body' => $request->getContent(), + 'signature' => $request->header('x-signature'), + 'webhookSecret' => config('services.facturapi.webhook_secret'), + ]); + + if (!$isValid['valid']) { + return response()->json(['error' => 'Invalid signature'], 401); + } + + // Process your webhook... + + return response()->json(['status' => 'ok']); + } +} +``` + +## 6. Laravel Octane (FrankenPHP, RoadRunner, Swoole) + +If you use a long-running application server like FrankenPHP, RoadRunner, or Swoole (via Laravel Octane), the SDK is perfectly safe to be registered as a `singleton` as long as you use a single API key for your entire application. The underlying HTTP client can be safely shared across requests. + +However, if your application is **multi-tenant** (e.g. you use a different Facturapi key depending on the authenticated user or tenant), you must register the SDK as a `scoped` instance rather than a `singleton`. This ensures a fresh instance is created per request: + +```php +// In AppServiceProvider +$this->app->scoped(Facturapi::class, function ($app) { + // Example: fetching the key dynamically based on the current tenant + $tenantKey = $app->make(CurrentTenant::class)->facturapi_key; + return new Facturapi($tenantKey); +}); +``` diff --git a/docs/symfony.md b/docs/symfony.md new file mode 100644 index 0000000..acf4041 --- /dev/null +++ b/docs/symfony.md @@ -0,0 +1,96 @@ +# Integration with Symfony + +This document shows how to integrate the Facturapi SDK into a Symfony project. + +## 1. Environment Variables Configuration + +Add your Facturapi API key to your `.env` or `.env.local` file: + +```env +FACTURAPI_KEY=sk_test_your_api_key +FACTURAPI_WEBHOOK_SECRET=your_webhook_secret +``` + +## 2. Services Configuration + +Configure dependency injection in `config/services.yaml` so Symfony knows how to instantiate the `Facturapi` class: + +```yaml +# config/services.yaml +services: + # ... + + Facturapi\Facturapi: + arguments: + $apiKey: '%env(FACTURAPI_KEY)%' +``` + +## 3. Usage in a Controller + +With the configuration above, Symfony will automatically inject the configured instance into your controllers: + +```php +namespace App\Controller; + +use Facturapi\Facturapi; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\Routing\Annotation\Route; + +class InvoiceController extends AbstractController +{ + #[Route('/customers', name: 'app_customers')] + public function index(Facturapi $facturapi): JsonResponse + { + // Usage example: List customers + $customers = $facturapi->customers()->all(); + + return $this->json($customers); + } +} +``` + +## 4. Webhook Validation + +You can validate incoming webhooks locally using the `validateSignature` method: + +```php +namespace App\Controller; + +use Facturapi\Facturapi; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\DependencyInjection\Attribute\Autowire; + +class WebhookController extends AbstractController +{ + #[Route('/webhook', name: 'app_webhook', methods: ['POST'])] + public function handle( + Request $request, + Facturapi $facturapi, + #[Autowire(env: 'FACTURAPI_WEBHOOK_SECRET')] string $webhookSecret + ): JsonResponse { + $isValid = $facturapi->webhooks()->validateSignature([ + 'body' => $request->getContent(), + 'signature' => $request->headers->get('x-signature'), + 'webhookSecret' => $webhookSecret, + ]); + + if (!$isValid['valid']) { + return $this->json(['error' => 'Invalid signature'], 401); + } + + // Process your webhook... + + return $this->json(['status' => 'ok']); + } +} +``` + +## 5. Long-Running Processes (FrankenPHP, RoadRunner, Swoole) + +If you are serving your Symfony application using FrankenPHP, RoadRunner, or Swoole, the SDK is safe to be registered as a standard service (which is a singleton by default in Symfony) as it contains no request-specific state. + +However, if you are building a **multi-tenant** application where the API key changes per request, you should avoid injecting the SDK as a singleton. Instead, instantiate `Facturapi` dynamically inside your controllers or inject it via a factory that reads the tenant's API key from the request context. diff --git a/example.php b/example.php deleted file mode 100644 index 847356c..0000000 --- a/example.php +++ /dev/null @@ -1,95 +0,0 @@ -Invoices->retrieve( "5a3f54cff508333611ad6b40" ) ); - -$invoice = array( - "type" => \Facturapi\InvoiceType::EGRESO, - "customer" => "5a3ff03bf508333611ad6b44", - "items" => array( - array( - "quantity" => 1, - "product" => "5a3f4890f508333611ad6b3f" - ), - array( - "quantity" => 2, - "product" => array( - "description" => "Guitarra", - "product_key" => "01234567", - "price" => 420.69, - "sku" => "ABC4567" - ) - ) - ), - "payment_form" => \Facturapi\PaymentForm::EFECTIVO, - "relation" => \Facturapi\InvoiceRelation::DEVOLUCION, - "related" => [ 'UUID_de_factura_relacionada' ], - "folio_number" => "581", - "series" => "F" -); - -var_dump( $facturapi->Invoices->create( $invoice ) ); - -var_dump( $facturapi->Invoices->retrieve( "59914af9b1bece552fcaaafd" ) ); - -var_dump( $facturapi->Invoices->all() ); - - -var_dump( $facturapi->Products->all() ); - -$product = array( - "description" => "Hukulele", - "product_key" => "60131303", - "price" => 345.60, - "sku" => "ABC1234" -); - -var_dump( $facturapi->Products->create( $product ) ); - -$product = array( - "description" => "Guitarra" -); - -$updated_product = $facturapi->Products->update( "5a3f3e35f508333611ad6b3e", $product ); -var_dump( $updated_product ); - -$facturapi->Products->delete( "5a3f3e35f508333611ad6b3e" ); - -var_dump( $facturapi->Customers->all() ); - - -var_dump( $facturapi->Customers->retrieve( "5a3ee743f508333611ad6b3c" ) ); - - -$customer = array( - "email" => "test@test.com", - "legal_name" => "Testa Mesta", - "tax_id" => "RFC", - "address" => array( - "zip" => "44940", - "street" => "Sunset Blvd" - ) -); - -$new_customer = $facturapi->Customers->create( $customer ); -var_dump( $new_customer ); - - -$customer = array( - "email" => "testa@mestapapa.com", - "legal_name" => "Testa Mesta Papa", -); - -$updated_customer = $facturapi->Customers->update( "5a3ee743f508333611ad6b3c", $customer ); -var_dump( $updated_customer ); - - -$facturapi->Customers->delete( "5a3fefd9f508333611ad6b43" ); diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..879e723 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,31 @@ +includes: + - vendor/phpstan/phpstan-strict-rules/rules.neon + - vendor/phpstan/phpstan-deprecation-rules/rules.neon + +parameters: + level: 9 + paths: + - src + tmpDir: build/phpstan + treatPhpDocTypesAsCertain: false + ignoreErrors: + - + identifier: missingType.iterableValue + - + identifier: property.phpDocType + - + identifier: return.phpDocType + - + identifier: missingType.parameter + - + identifier: argument.type + - + identifier: parameter.notFound + - + identifier: notIdentical.alwaysTrue + - + identifier: nullsafe.neverNull + - + identifier: class.notFound + parallel: + maximumNumberOfProcesses: 1 diff --git a/pint.json b/pint.json new file mode 100644 index 0000000..17ed0b1 --- /dev/null +++ b/pint.json @@ -0,0 +1,12 @@ +{ + "preset": "psr12", + "rules": { + "declare_strict_types": true, + "strict_param": true, + "array_syntax": { + "syntax": "short" + }, + "no_unused_imports": true, + "single_quote": true + } +} diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..2ef20a5 --- /dev/null +++ b/rector.php @@ -0,0 +1,22 @@ +paths([ + __DIR__ . '/src', + ]); + + $rectorConfig->sets([ + LevelSetList::UP_TO_PHP_82, + SetList::TYPE_DECLARATION, + SetList::CODE_QUALITY, + SetList::DEAD_CODE, + SetList::PRIVATIZATION, + SetList::EARLY_RETURN, + ]); +}; diff --git a/retention.pdf b/retention.pdf deleted file mode 100644 index 17bd6da..0000000 Binary files a/retention.pdf and /dev/null differ diff --git a/src/CartaPorteConstants.php b/src/CartaPorteConstants.php deleted file mode 100644 index 48e1f89..0000000 --- a/src/CartaPorteConstants.php +++ /dev/null @@ -1,564 +0,0 @@ - 'Importacion definitiva', - self::DEFINITIVE_EXPORT => 'Exportacion definitiva', - self::INTERNAL_MERCHANDISE_TRANSIT => 'Transito interno de mercancias', - self::INTERNAL_MERCHANDISE_TRANSIT_FOR_EXPORT => 'Transito interno de mercancias para exportacion', - self::EXTERNAL_MERCHANDISE_TRANSIT => 'Transito externo de mercancias', - self::EXTERNAL_MERCHANDISE_TRANSIT_FOR_EXPORT => 'Transito externo de mercancias para exportacion', - self::FISCAL_WAREHOUSE => 'Deposito fiscal', - self::STRATEGIC_FISCAL_ENCLOSURE => 'Recinto fiscalizado estrategico', - self::FISCAL_ENCLOSURE => 'Recinto fiscalizado', - self::CUSTOMS_TRANSIT => 'Transito aduanero', - ]; -} - -class CveTransporteEnum { - const AUTOTRANSPORT = '01'; - const NAVY_TRANSPORT = '02'; - const AIRLINE_TRANSPORT = '03'; - const RAIL_TRANSPORT = '04'; - const OTHER = '05'; - - const DESCRIPTION = [ - self::AUTOTRANSPORT => 'Autotransporte', - self::NAVY_TRANSPORT => 'Transporte Maritimo', - self::AIRLINE_TRANSPORT => 'Transporte Aereo', - self::RAIL_TRANSPORT => 'Transporte Ferroviario', - self::OTHER => 'Otro', - ]; -} - -class TipoEstacionEnum { - const NATIONAL_ORIGIN = '01'; - const INTERMEDIATE = '02'; - const FINAL_DESTINATION = '03'; - - const DESCRIPTION = [ - self::NATIONAL_ORIGIN => 'Origen Nacional', - self::INTERMEDIATE => 'Intermedia', - self::FINAL_DESTINATION => 'Destino Final Nacional', - ]; -} - -class PermisoSctEnum { - const FEDERAL_TRANSPORT_OF_LOAD = 'TPAF01'; - const PRIVATE_TRANSPORT_OF_LOAD = 'TPAF02'; - const FEDERAL_SPECIALIZED_HAZARDOUS_MATERIALS = 'TPAF03'; - const TRANSPORT_OF_AUTOMOBILES = 'TPAF04'; - const TRANSPORT_OF_HEAVY_LOAD_UP_TO_90_TONS = 'TPAF05'; - const TRANSPORT_OF_SPECIALIZED_HEAVY_LOAD_OVER_90_TONS = 'TPAF06'; - const PRIVATE_HAZARDOUS_MATERIALS_TRANSPORT = 'TPAF07'; - const INTERNATIONAL_LONG_HAUL_TRANSPORT = 'TPAF08'; - const INTERNATIONAL_SPECIALIZED_HAZARDOUS_LONG_HAUL = 'TPAF09'; - const FEDERAL_TRANSPORT_US_BORDER_ZONE = 'TPAF10'; - const FEDERAL_SPECIALIZED_US_BORDER_ZONE = 'TPAF11'; - const AUXILIARY_TOWING_SERVICE = 'TPAF12'; - const AUXILIARY_TOWING_AND_STORAGE_SERVICE = 'TPAF13'; - const PACKAGING_AND_COURIER_SERVICE = 'TPAF14'; - const SPECIAL_TRANSPORT_INDUSTRIAL_CRANES_UP_TO_90_TONS = 'TPAF15'; - const FEDERAL_RENTAL_COMPANIES_SERVICE = 'TPAF16'; - const VEHICLE_MOVERS_NEW_VEHICLES = 'TPAF17'; - const MANUFACTURERS_DISTRIBUTORS_NEW_VEHICLES = 'TPAF18'; - const AUTHORIZATION_DOUBLE_ARTICULATED_TRUCK = 'TPAF19'; - const FEDERAL_SPECIALIZED_FUNDS_AND_VALUES = 'TPAF20'; - const TEMPORARY_CABOTAGE_NAVIGATION = 'TPTM01'; - const NATIONAL_INTERNATIONAL_REGULAR_SERVICE_MEXICAN = 'TPTA01'; - const FOREIGN_COMPANIES_REGULAR_AIR_SERVICE = 'TPTA02'; - const NATIONAL_INTERNATIONAL_CHARTER_SERVICE = 'TPTA03'; - const NATIONAL_INTERNATIONAL_AIR_TAXI_SERVICE = 'TPTA04'; - const NOT_IN_CATALOG = 'TPXX00'; - - const DESCRIPTION = [ - self::FEDERAL_TRANSPORT_OF_LOAD => 'Autotransporte Federal de carga general.', - self::PRIVATE_TRANSPORT_OF_LOAD => 'Transporte privado de carga.', - self::FEDERAL_SPECIALIZED_HAZARDOUS_MATERIALS => 'Autotransporte Federal de Carga Especializada de materiales y residuos peligrosos.', - self::TRANSPORT_OF_AUTOMOBILES => 'Transporte de automoviles sin rodar en vehiculo tipo gondola.', - self::TRANSPORT_OF_HEAVY_LOAD_UP_TO_90_TONS => 'Transporte de carga de gran peso y/o volumen de hasta 90 toneladas.', - self::TRANSPORT_OF_SPECIALIZED_HEAVY_LOAD_OVER_90_TONS => 'Transporte de carga especializada de gran peso y/o volumen de mas 90 toneladas.', - self::PRIVATE_HAZARDOUS_MATERIALS_TRANSPORT => 'Transporte Privado de materiales y residuos peligrosos.', - self::INTERNATIONAL_LONG_HAUL_TRANSPORT => 'Autotransporte internacional de carga de largo recorrido.', - self::INTERNATIONAL_SPECIALIZED_HAZARDOUS_LONG_HAUL => 'Autotransporte internacional de carga especializada de materiales y residuos peligrosos de largo recorrido.', - self::FEDERAL_TRANSPORT_US_BORDER_ZONE => 'Autotransporte Federal de Carga General cuyo ambito de aplicacion comprende la franja fronteriza con Estados Unidos.', - self::FEDERAL_SPECIALIZED_US_BORDER_ZONE => 'Autotransporte Federal de Carga Especializada cuyo ambito de aplicacion comprende la franja fronteriza con Estados Unidos.', - self::AUXILIARY_TOWING_SERVICE => 'Servicio auxiliar de arrastre en las vias generales de comunicacion.', - self::AUXILIARY_TOWING_AND_STORAGE_SERVICE => 'Servicio auxiliar de servicios de arrastre, arrastre y salvamento, y deposito de vehiculos en las vias generales de comunicacion.', - self::PACKAGING_AND_COURIER_SERVICE => 'Servicio de paqueteria y mensajeria en las vias generales de comunicacion.', - self::SPECIAL_TRANSPORT_INDUSTRIAL_CRANES_UP_TO_90_TONS => 'Transporte especial para el transito de gruas industriales con peso maximo de 90 toneladas.', - self::FEDERAL_RENTAL_COMPANIES_SERVICE => 'Servicio federal para empresas arrendadoras servicio publico federal.', - self::VEHICLE_MOVERS_NEW_VEHICLES => 'Empresas trasladistas de vehiculos nuevos.', - self::MANUFACTURERS_DISTRIBUTORS_NEW_VEHICLES => 'Empresas fabricantes o distribuidoras de vehiculos nuevos.', - self::AUTHORIZATION_DOUBLE_ARTICULATED_TRUCK => 'Autorizacion expresa para circular en los caminos y puentes de jurisdiccion federal con configuraciones de tractocamion doblemente articulado.', - self::FEDERAL_SPECIALIZED_FUNDS_AND_VALUES => 'Autotransporte Federal de Carga Especializada de fondos y valores.', - self::TEMPORARY_CABOTAGE_NAVIGATION => 'Permiso temporal para navegacion de cabotaje', - self::NATIONAL_INTERNATIONAL_REGULAR_SERVICE_MEXICAN => 'Concesion y/o autorizacion para el servicio regular nacional y/o internacional para empresas mexicanas', - self::FOREIGN_COMPANIES_REGULAR_AIR_SERVICE => 'Permiso para el servicio aereo regular de empresas extranjeras', - self::NATIONAL_INTERNATIONAL_CHARTER_SERVICE => 'Permiso para el servicio nacional e internacional no regular de fletamento', - self::NATIONAL_INTERNATIONAL_AIR_TAXI_SERVICE => 'Permiso para el servicio nacional e internacional no regular de taxi aereo', - self::NOT_IN_CATALOG => 'Permiso no contemplado en el catalogo.', - ]; -} - -class SectorCofeprisEnum { - const MEDICINE = '01'; - const PRECURSORS_AND_DUAL_USE_CHEMICALS = '02'; - const PSYCHOTROPIC_AND_NARCOTIC = '03'; - const TOXIC_SUBSTANCES = '04'; - const PESTICIDES_AND_FERTILIZERS = '05'; - - const DESCRIPTION = [ - self::MEDICINE => 'Medicamento', - self::PRECURSORS_AND_DUAL_USE_CHEMICALS => 'Precursores y quimicos de uso dual', - self::PSYCHOTROPIC_AND_NARCOTIC => 'Psicotropicos y estupefacientes', - self::TOXIC_SUBSTANCES => 'Sustancias toxicas', - self::PESTICIDES_AND_FERTILIZERS => 'Plaguicidas y fertilizantes', - ]; -} - -class PharmaceuticalFormsEnum { - const TABLET = '01'; - const CAPSULES = '02'; - const COMPRESSED = '03'; - const SUGAR_COATED = '04'; - const SUSPENSION = '05'; - const SOLUTION = '06'; - const EMULSION = '07'; - const SYRUP = '08'; - const INJECTABLE = '09'; - const CREAM = '10'; - const OINTMENT = '11'; - const AEROSOL = '12'; - const MEDICINAL_GAS = '13'; - const GEL = '14'; - const IMPLANT = '15'; - const OVULE = '16'; - const PATCH = '17'; - const PASTE = '18'; - const POWDER = '19'; - const SUPPOSITORY = '20'; - - const DESCRIPTION = [ - self::TABLET => 'Tableta', - self::CAPSULES => 'Capsulas', - self::COMPRESSED => 'Comprimidos', - self::SUGAR_COATED => 'Grageas', - self::SUSPENSION => 'Suspension', - self::SOLUTION => 'Solucion', - self::EMULSION => 'Emulsion', - self::SYRUP => 'Jarabe', - self::INJECTABLE => 'Inyectable', - self::CREAM => 'Crema', - self::OINTMENT => 'Unguento', - self::AEROSOL => 'Aerosol', - self::MEDICINAL_GAS => 'Gas medicinal', - self::GEL => 'Gel', - self::IMPLANT => 'Implante', - self::OVULE => 'Ovulo', - self::PATCH => 'Parche', - self::PASTE => 'Pasta', - self::POWDER => 'Polvo', - self::SUPPOSITORY => 'Supositorio', - ]; -} - -class SpecialConditionsEnum { - const FROZEN = '01'; - const REFRIGERATED = '02'; - const CONTROLLED_TEMPERATURE = '03'; - const ROOM_TEMPERATURE = '04'; - - const DESCRIPTION = [ - self::FROZEN => 'Congelados', - self::REFRIGERATED => 'Refrigerados', - self::CONTROLLED_TEMPERATURE => 'Temperatura controlada', - self::ROOM_TEMPERATURE => 'Temperatura ambiente', - ]; -} - -class MaterialTypeEnum { - const RAW_MATERIAL = '01'; - const PROCESSED_MATERIAL = '02'; - const FINISHED_MATERIAL = '03'; - const MANUFACTURING_INDUSTRY_MATERIAL = '04'; - const OTHER = '05'; - - const DESCRIPTION = [ - self::RAW_MATERIAL => 'Materia prima', - self::PROCESSED_MATERIAL => 'Materia procesada', - self::FINISHED_MATERIAL => 'Materia terminada (producto terminado)', - self::MANUFACTURING_INDUSTRY_MATERIAL => 'Materia para la industria manufacturera', - self::OTHER => 'Otra', - ]; -} - -class TypeOfCustomsDocumentEnum { - const PEDIMENT = '01'; - const TEMPORARY_IMPORT_AUTHORIZATION = '02'; - const TEMPORARY_IMPORT_AUTHORIZATION_VESSELS = '03'; - const TEMPORARY_IMPORT_AUTHORIZATION_MAINTENANCE = '04'; - const IMPORT_AUTHORIZATION_SPECIAL_VEHICLES = '05'; - const TEMPORARY_EXPORT_NOTICE = '06'; - const TRANSFER_NOTICE_IMMEX_RFE_AUTHORIZED_OPERATOR = '07'; - const TRANSFER_NOTICE_AUTO_PARTS_BORDER_ZONE = '08'; - const TEMPORARY_IMPORT_CONSTANCY_CONTAINERS = '09'; - const MERCHANDISE_TRANSFER_CONSTANCY = '10'; - const DONATION_AUTHORIZATION_FOREIGN_MERCHANDISE = '11'; - const ATA_CARNET = '12'; - const EXCHANGE_LISTS = '13'; - const TEMPORARY_IMPORT_PERMIT = '14'; - const TEMPORARY_IMPORT_PERMIT_RV = '15'; - const TEMPORARY_IMPORT_PERMIT_VESSELS = '16'; - const DONATION_REQUEST_EMERGENCIES_DISASTERS = '17'; - const CONSOLIDATED_NOTICE = '18'; - const CROSSING_NOTICE_MERCHANDISE = '19'; - const OTHER = '20'; - - const DESCRIPTION = [ - self::PEDIMENT => 'Pedimento', - self::TEMPORARY_IMPORT_AUTHORIZATION => 'Autorizacion de importacion temporal', - self::TEMPORARY_IMPORT_AUTHORIZATION_VESSELS => 'Autorizacion de importacion temporal de embarcaciones', - self::TEMPORARY_IMPORT_AUTHORIZATION_MAINTENANCE => 'Autorizacion de importacion temporal de mercancias, destinadas al mantenimiento y reparacion de las mercancias importadas temporalmente', - self::IMPORT_AUTHORIZATION_SPECIAL_VEHICLES => 'Autorizacion para la importacion de vehiculos especialmente construidos o transformados, equipados con dispositivos o aparatos diversos para cumplir con contrato derivado de licitacion publica', - self::TEMPORARY_EXPORT_NOTICE => 'Aviso de exportacion temporal', - self::TRANSFER_NOTICE_IMMEX_RFE_AUTHORIZED_OPERATOR => 'Aviso de traslado de mercancias de empresas con Programa IMMEX, RFE u Operador Economico Autorizado', - self::TRANSFER_NOTICE_AUTO_PARTS_BORDER_ZONE => 'Aviso para el traslado de autopartes ubicadas en la franja o region fronteriza a la industria terminal automotriz o manufacturera de vehiculos de autotransporte en el resto del territorio nacional', - self::TEMPORARY_IMPORT_CONSTANCY_CONTAINERS => 'Constancia de importacion temporal, retorno o transferencia de contenedores', - self::MERCHANDISE_TRANSFER_CONSTANCY => 'Constancia de transferencia de mercancias', - self::DONATION_AUTHORIZATION_FOREIGN_MERCHANDISE => 'Autorizacion de donacion de mercancias al Fisco Federal que se encuentren en el extranjero', - self::ATA_CARNET => 'Cuaderno ATA', - self::EXCHANGE_LISTS => 'Listas de intercambio', - self::TEMPORARY_IMPORT_PERMIT => 'Permiso de Importacion Temporal', - self::TEMPORARY_IMPORT_PERMIT_RV => 'Permiso de importacion temporal de casa rodante', - self::TEMPORARY_IMPORT_PERMIT_VESSELS => 'Permiso de importacion temporal de embarcaciones', - self::DONATION_REQUEST_EMERGENCIES_DISASTERS => 'Solicitud de donacion de mercancias en casos de emergencias o desastres naturales', - self::CONSOLIDATED_NOTICE => 'Aviso de consolidado', - self::CROSSING_NOTICE_MERCHANDISE => 'Aviso de cruce de mercancias', - self::OTHER => 'Otro', - ]; -} - -class TransportTypeEnum { - const UNIT_TRUCK = 'PT01'; - const TRUCK = 'PT02'; - const TRACTOR_TRUCK = 'PT03'; - const TRAILER = 'PT04'; - const SEMI_TRAILER = 'PT05'; - const LIGHT_LOAD_VEHICLE = 'PT06'; - const CRANE = 'PT07'; - const AIRCRAFT = 'PT08'; - const SHIP_OR_VESSEL = 'PT09'; - const CAR_OR_WAGON = 'PT10'; - const CONTAINER = 'PT11'; - const LOCOMOTIVE = 'PT12'; - - const DESCRIPTION = [ - self::UNIT_TRUCK => 'Camion unitario', - self::TRUCK => 'Camion', - self::TRACTOR_TRUCK => 'Tractocamion', - self::TRAILER => 'Remolque', - self::SEMI_TRAILER => 'Semirremolque', - self::LIGHT_LOAD_VEHICLE => 'Vehiculo ligero de carga', - self::CRANE => 'Grua', - self::AIRCRAFT => 'Aeronave', - self::SHIP_OR_VESSEL => 'Barco o buque', - self::CAR_OR_WAGON => 'Carro o vagon', - self::CONTAINER => 'Contenedor', - self::LOCOMOTIVE => 'Locomotora', - ]; -} - -class TransportFigureEnum { - const OPERATOR = '01'; - const OWNER = '02'; - const LESSOR = '03'; - const NOTIFIED = '04'; - const COORDINATED_MEMBER = '05'; - - const DESCRIPTION = [ - self::OPERATOR => 'Operador', - self::OWNER => 'Propietario', - self::LESSOR => 'Arrendador', - self::NOTIFIED => 'Notificado', - self::COORDINATED_MEMBER => 'Integrante de Coordinados', - ]; -} - -class RegistroIstmoEnum { - const COATZACOALCOS_I = '01'; - const COATZACOALCOS_II = '02'; - const TEXISTEPEC = '03'; - const SAN_JUAN_EVANGELISTA = '04'; - const SALINA_CRUZ = '05'; - const SAN_BLAS_ATEMPA = '06'; - - const DESCRIPTION = [ - self::COATZACOALCOS_I => 'Coatzacoalcos I', - self::COATZACOALCOS_II => 'Coatzacoalcos II', - self::TEXISTEPEC => 'Texistepec', - self::SAN_JUAN_EVANGELISTA => 'San Juan Evangelista', - self::SALINA_CRUZ => 'Salina Cruz', - self::SAN_BLAS_ATEMPA => 'San Blas Atempa', - ]; -} - -class LoadingKey { - const GENERAL_LOOSE_CARGO = 'CGS'; - const GENERAL_CONTAINERIZED_CARGO = 'CGC'; - const BULK_MINERAL = 'GMN'; - const AGRICULTURAL_BULK = 'GAG'; - const OTHER_FLUIDS = 'OFL'; - const OIL_AND_DERIVATIVES = 'PYD'; - - const DESCRIPTION = [ - self::GENERAL_LOOSE_CARGO => 'Carga General Suelta', - self::GENERAL_CONTAINERIZED_CARGO => 'Carga General Contenerizada', - self::BULK_MINERAL => 'Gran Mineral', - self::AGRICULTURAL_BULK => 'Granel Agricola', - self::OTHER_FLUIDS => 'Otros Fluidos', - self::OIL_AND_DERIVATIVES => 'Petroleo y Derivados', - ]; -} - -class ConfigMaritimaEnum { - const SUPPLIER = 'B01'; - const BARGE = 'B02'; - const BULK_CARRIER = 'B03'; - const CONTAINER_SHIP = 'B04'; - const DREDGER = 'B05'; - const FISHING = 'B06'; - const GENERAL_CARGO = 'B07'; - const CHEMICAL_TANKER = 'B08'; - const FERRY = 'B09'; - const RO_RO = 'B10'; - const RESEARCH = 'B11'; - const TANKER = 'B12'; - const GAS_CARRIER = 'B13'; - const TUG = 'B14'; - const EXTRAORDINARY_SPECIALIZATION = 'B15'; - - const DESCRIPTION = [ - self::SUPPLIER => 'Abastecedor', - self::BARGE => 'Barcaza', - self::BULK_CARRIER => 'Granelero', - self::CONTAINER_SHIP => 'Porta Contenedor', - self::DREDGER => 'Draga', - self::FISHING => 'Pesquero', - self::GENERAL_CARGO => 'Carga General', - self::CHEMICAL_TANKER => 'Quimiqueros', - self::FERRY => 'Transbordadores', - self::RO_RO => 'Carga RoRo', - self::RESEARCH => 'Investigacion', - self::TANKER => 'Tanquero', - self::GAS_CARRIER => 'Gasero', - self::TUG => 'Remolcador', - self::EXTRAORDINARY_SPECIALIZATION => 'Extraordinaria especializacion', - ]; -} - -class RailTrafficTypeEnum { - const LOCAL_TRAFFIC = 'TT01'; - const INTERLINE_FORWARDED_TRAFFIC = 'TT02'; - const INTERLINE_RECEIVED_TRAFFIC = 'TT03'; - const INTERLINE_TRANSIT_TRAFFIC = 'TT04'; - - const DESCRIPTION = [ - self::LOCAL_TRAFFIC => 'Trafico local', - self::INTERLINE_FORWARDED_TRAFFIC => 'Trafico interlineal remitido', - self::INTERLINE_RECEIVED_TRAFFIC => 'Trafico interlineal recibido', - self::INTERLINE_TRANSIT_TRAFFIC => 'Trafico interlineal en transito', - ]; -} - -class ContainerTypeEnum { - const CONTAINER_20FT = 'TC01'; - const CONTAINER_40FT = 'TC02'; - const CONTAINER_45FT = 'TC03'; - const CONTAINER_48FT = 'TC04'; - const CONTAINER_53FT = 'TC05'; - - const DESCRIPTION = [ - self::CONTAINER_20FT => 'Contenedor de 6.1 Mts de longitud', - self::CONTAINER_40FT => 'Contenedor de 12.2 Mts de longitud', - self::CONTAINER_45FT => 'Contenedor de 13.7 Mts de longitud', - self::CONTAINER_48FT => 'Contenedor de 14.6 Mts de longitud', - self::CONTAINER_53FT => 'Contenedor de 16.1 Mts de longitud', - ]; -} - -class MaritimeContainerTypeEnum { - const REFRIGERATED_20FT = 'CM001'; - const REFRIGERATED_40FT = 'CM002'; - const STANDARD_8FT = 'CM003'; - const STANDARD_10FT = 'CM004'; - const STANDARD_20FT = 'CM005'; - const STANDARD_40FT = 'CM006'; - const OPEN_SIDE = 'CM007'; - const ISOTANK = 'CM008'; - const FLAT_RACKS = 'CM009'; - const TANKER_SHIP = 'CM010'; - const FERRY = 'CM011'; - const TOURIST_FERRY = 'CM012'; - - const DESCRIPTION = [ - self::REFRIGERATED_20FT => 'Contenedores refrigerados de 20FT', - self::REFRIGERATED_40FT => 'Contenedores refrigerados de 40FT', - self::STANDARD_8FT => 'Contenedores estandar de 8FT', - self::STANDARD_10FT => 'Contenedores estandar de 10FT', - self::STANDARD_20FT => 'Contenedores estandar de 20FT', - self::STANDARD_40FT => 'Contenedores estandar de 40FT', - self::OPEN_SIDE => 'Contenedores Open Side', - self::ISOTANK => 'Contenedor Isotanque', - self::FLAT_RACKS => 'Contenedor flat racks', - self::TANKER_SHIP => 'Buque tanque', - self::FERRY => 'Ferri', - self::TOURIST_FERRY => 'Ferri - Turistico y vacios', - ]; -} - -class RailCarTypeEnum { - const BOXCAR = 'TC01'; - const GONDOLA = 'TC02'; - const HOPPER = 'TC03'; - const TANK = 'TC04'; - const INTERMODAL_PLATFORM = 'TC05'; - const GENERAL_PURPOSE_PLATFORM = 'TC06'; - const AUTOMOTIVE_PLATFORM = 'TC07'; - const LOCOMOTIVE = 'TC08'; - const SPECIAL_CAR = 'TC09'; - const PASSENGER = 'TC10'; - const TRACK_MAINTENANCE = 'TC11'; - - const DESCRIPTION = [ - self::BOXCAR => 'Furgon', - self::GONDOLA => 'Gondola', - self::HOPPER => 'Tolva', - self::TANK => 'Tanque', - self::INTERMODAL_PLATFORM => 'Plataforma Intermodal', - self::GENERAL_PURPOSE_PLATFORM => 'Plataforma de Uso General', - self::AUTOMOTIVE_PLATFORM => 'Plataforma Automotriz', - self::LOCOMOTIVE => 'Locomotora', - self::SPECIAL_CAR => 'Carro Especial', - self::PASSENGER => 'Pasajeros', - self::TRACK_MAINTENANCE => 'Mantenimiento de Via', - ]; -} - -class RailServiceTypeEnum { - const RAILWAY_CARS = 'TS01'; - const INTERMODAL_RAILWAY_CARS = 'TS02'; - const UNIT_TRAIN_RAILWAY_CARS = 'TS03'; - const UNIT_TRAIN_INTERMODAL = 'TS04'; -} - -class MotivoTrasladoEnum { - const PRIORLY_INVOICED_GOODS_SHIPMENT = '01'; - const RELOCATION_OF_OWN_GOODS = '02'; - const CONSIGNMENT_CONTRACT_GOODS_SHIPMENT = '03'; - const GOODS_SHIPMENT_FOR_SUBSEQUENT_SALE = '04'; - const THIRD_PARTY_OWNED_GOODS_SHIPMENT = '05'; - const OTHER = '99'; - - const DESCRIPTION = [ - self::PRIORLY_INVOICED_GOODS_SHIPMENT => 'Envio de mercancias facturadas con anterioridad', - self::RELOCATION_OF_OWN_GOODS => 'Reubicacion de mercancias propias', - self::CONSIGNMENT_CONTRACT_GOODS_SHIPMENT => 'Envio de mercancias objeto de contrato de consignacion', - self::GOODS_SHIPMENT_FOR_SUBSEQUENT_SALE => 'Envio de mercancias para posterior enajenacion', - self::THIRD_PARTY_OWNED_GOODS_SHIPMENT => 'Envio de mercancias propiedad de terceros', - self::OTHER => 'Otros', - ]; -} - -class IncotermEnum { - const CFR = 'CFR'; - const CIF = 'CIF'; - const CPT = 'CPT'; - const CIP = 'CIP'; - const DAP = 'DAP'; - const DDP = 'DDP'; - const DPU = 'DPU'; - const EXW = 'EXW'; - const FCA = 'FCA'; - const FAS = 'FAS'; - const FOB = 'FOB'; - - const DESCRIPTION = [ - self::CFR => 'Coste y flete (puerto de destino convenido).', - self::CIF => 'Coste, seguro y flete (puerto de destino convenido).', - self::CPT => 'Transporte pagado hasta (el lugar de destino convenido).', - self::CIP => 'Transporte y seguro pagados hasta (lugar de destino convenido).', - self::DAP => 'Entregada en lugar.', - self::DDP => 'Entregada derechos pagados (lugar de destino convenido).', - self::DPU => 'Entregada y descargada en lugar acordado.', - self::EXW => 'En fabrica (lugar convenido).', - self::FCA => 'Franco transportista (lugar designado).', - self::FAS => 'Franco al costado del buque (puerto de carga convenido).', - self::FOB => 'Franco a bordo (puerto de carga convenido).', - ]; -} - -class UnidadAduanaEnum { - const KILO = '01'; - const GRAMO = '02'; - const METRO_LINEAL = '03'; - const METRO_CUADRADO = '04'; - const METRO_CUBICO = '05'; - const PIEZA = '06'; - const CABEZA = '07'; - const LITRO = '08'; - const PAR = '09'; - const KILOWATT = '10'; - const MILLAR = '11'; - const JUEGO = '12'; - const KILOWATT_HORA = '13'; - const TONELADA = '14'; - const BARRIL = '15'; - const GRAMO_NETO = '16'; - const DECENAS = '17'; - const CIENTOS = '18'; - const DOCENAS = '19'; - const CAJA = '20'; - const BOTELLA = '21'; - const CARAT = '22'; - const SERVICIO = '99'; - - const DESCRIPTION = [ - self::KILO => 'KILO', - self::GRAMO => 'GRAMO', - self::METRO_LINEAL => 'METRO LINEAL', - self::METRO_CUADRADO => 'METRO CUADRADO', - self::METRO_CUBICO => 'METRO CUBICO', - self::PIEZA => 'PIEZA', - self::CABEZA => 'CABEZA', - self::LITRO => 'LITRO', - self::PAR => 'PAR', - self::KILOWATT => 'KILOWATT', - self::MILLAR => 'MILLAR', - self::JUEGO => 'JUEGO', - self::KILOWATT_HORA => 'KILOWATT/HORA', - self::TONELADA => 'TONELADA', - self::BARRIL => 'BARRIL', - self::GRAMO_NETO => 'GRAMO NETO', - self::DECENAS => 'DECENAS', - self::CIENTOS => 'CIENTOS', - self::DOCENAS => 'DOCENAS', - self::CAJA => 'CAJA', - self::BOTELLA => 'BOTELLA', - self::CARAT => 'CARAT', - self::SERVICIO => 'SERVICIO', - ]; -} diff --git a/src/Enums/BaseTaxFactorEnum.php b/src/Enums/BaseTaxFactorEnum.php new file mode 100644 index 0000000..5d9b763 --- /dev/null +++ b/src/Enums/BaseTaxFactorEnum.php @@ -0,0 +1,12 @@ +errorData = $errorData; - $this->statusCode = $statusCode; - $this->rawBody = $rawBody; - $this->headers = $headers; - } - - 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; - } - - public function getErrorCode(): ?string - { - if (!is_array($this->errorData) || !array_key_exists('code', $this->errorData)) { - return null; - } - - $code = $this->errorData['code']; - if ($code === null) { - return null; - } - - if (is_string($code)) { - return $code; - } - - return is_int($code) || is_float($code) ? (string) $code : null; - } - - public function getErrorPath(): ?string - { - return is_array($this->errorData) && is_string($this->errorData['path'] ?? null) - ? $this->errorData['path'] - : null; - } - - public function getErrorLocation(): ?string - { - return is_array($this->errorData) && is_string($this->errorData['location'] ?? null) - ? $this->errorData['location'] - : null; - } - - public function getErrors(): ?array - { - return is_array($this->errorData) && is_array($this->errorData['errors'] ?? null) - ? $this->errorData['errors'] - : null; - } - - public function getLogId(): ?string - { - return is_string($this->headers['x-facturapi-log-id'] ?? null) - ? $this->headers['x-facturapi-log-id'] - : null; - } - - public function getResponseHeaders(): array - { - return $this->headers; - } + public function __construct( + string $message = '', + int $code = 0, + ?Throwable $previous = null, + private mixed $errorData = null, + private readonly ?int $statusCode = null, + private readonly ?string $rawBody = null, + private array $headers = [] + ) { + parent::__construct($message, $code, $previous); + } + + 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; + } + + public function getErrorCode(): ?string + { + if (!is_array($this->errorData) || !array_key_exists('code', $this->errorData)) { + return null; + } + + $code = $this->errorData['code']; + if ($code === null) { + return null; + } + + if (is_string($code)) { + return $code; + } + + return is_int($code) || is_float($code) ? (string) $code : null; + } + + public function getErrorPath(): ?string + { + return is_array($this->errorData) && is_string($this->errorData['path'] ?? null) + ? $this->errorData['path'] + : null; + } + + public function getErrorLocation(): ?string + { + return is_array($this->errorData) && is_string($this->errorData['location'] ?? null) + ? $this->errorData['location'] + : null; + } + + public function getErrors(): ?array + { + return is_array($this->errorData) && is_array($this->errorData['errors'] ?? null) + ? $this->errorData['errors'] + : null; + } + + public function getLogId(): ?string + { + return is_string($this->headers['x-facturapi-log-id'] ?? null) + ? $this->headers['x-facturapi-log-id'] + : null; + } + + public function getResponseHeaders(): array + { + return $this->headers; + } } diff --git a/src/Exceptions/Facturapi_Exception.php b/src/Exceptions/Facturapi_Exception.php deleted file mode 100644 index 4f5b0d4..0000000 --- a/src/Exceptions/Facturapi_Exception.php +++ /dev/null @@ -1,9 +0,0 @@ -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->NominaCatalogs = new NominaCatalogs($apiKey, $config); - $this->Retentions = new Retentions($apiKey, $config); - $this->Tools = new Tools($apiKey, $config); - $this->Webhooks = new Webhooks($apiKey, $config); - } + private ?Customers $customers = null; + private ?Organizations $organizations = null; + private ?Products $products = null; + private ?Invoices $invoices = null; + private ?Receipts $receipts = null; + private ?Catalogs $catalogs = null; + private ?CartaPorteCatalogs $cartaPorteCatalogs = null; + private ?ComercioExteriorCatalogs $comercioExteriorCatalogs = null; + private ?NominaCatalogs $nominaCatalogs = null; + private ?Retentions $retentions = null; + private ?Tools $tools = null; + private ?Webhooks $webhooks = null; + + private readonly FacturapiHttpClient $httpClient; + + /** + * @param string $apiKey Facturapi API key. + * @param array{apiVersion?:string,httpClient?:\Psr\Http\Client\ClientInterface}|null $config + * Optional SDK config. + */ + public function __construct( + private readonly string $apiKey, + private readonly ?array $config = null + ) { + $this->httpClient = new FacturapiHttpClient($this->apiKey, $this->config); + } + + public function customers(): Customers + { + return $this->customers ??= new Customers($this->httpClient); + } + + public function organizations(): Organizations + { + return $this->organizations ??= new Organizations($this->httpClient); + } + + public function products(): Products + { + return $this->products ??= new Products($this->httpClient); + } + + public function invoices(): Invoices + { + return $this->invoices ??= new Invoices($this->httpClient); + } + + public function receipts(): Receipts + { + return $this->receipts ??= new Receipts($this->httpClient); + } + + public function catalogs(): Catalogs + { + return $this->catalogs ??= new Catalogs($this->httpClient); + } + + public function cartaPorteCatalogs(): CartaPorteCatalogs + { + return $this->cartaPorteCatalogs ??= new CartaPorteCatalogs($this->httpClient); + } + + public function comercioExteriorCatalogs(): ComercioExteriorCatalogs + { + return $this->comercioExteriorCatalogs ??= new ComercioExteriorCatalogs($this->httpClient); + } + + public function nominaCatalogs(): NominaCatalogs + { + return $this->nominaCatalogs ??= new NominaCatalogs($this->httpClient); + } + + public function retentions(): Retentions + { + return $this->retentions ??= new Retentions($this->httpClient); + } + + public function tools(): Tools + { + return $this->tools ??= new Tools($this->httpClient); + } + + public function webhooks(): Webhooks + { + return $this->webhooks ??= new Webhooks($this->httpClient); + } } diff --git a/src/Http/BaseClient.php b/src/Http/BaseClient.php deleted file mode 100644 index b40c2b8..0000000 --- a/src/Http/BaseClient.php +++ /dev/null @@ -1,593 +0,0 @@ -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 HTTP request. - * - * @return int|null - */ - public function getLastStatus(): ?int - { - return $this->lastStatus; - } - - /** - * Returns ENDPOINT that is set in specific api clients. All - * clients that extend BaseClient should set $ENDPOINT to the - * base path for the API (e.g.: the customers api sets the value to - * 'customers') - * - * @throws FacturapiException - */ - protected function getEndpoint(): string - { - if (empty($this->ENDPOINT)) { - throw new FacturapiException('ENDPOINT must be defined'); - } - - return $this->ENDPOINT; - } - - /** - * Returns API_VERSION that is set in specific api clients. All - * clients that extend BaseClient should set $API_VERSION to the - * version that the client is developed for (e.g.: the customers v1 - * client sets the value to 'v1') - * - * @throws FacturapiException - */ - protected function getApiVersion(): string - { - if (empty($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 array|string|null $params Path segment or query parameters. - * @param array|null $query Query parameters for nested endpoints. - * @return string - */ - protected function getRequestUrl($params = null, $query = null): string - { - $param_string = $params == null ? "" : ( - is_string($params) - ? ($query == null - ? "/" . $params - : "/" . $params . $this->arrayToParams($query) - ) : $this->arrayToParams($params) - ); - - return $this->BASE_URL . $this->getApiVersion() . "/" . $this->getEndpoint() . $param_string; - } - - /** - * Executes HTTP GET request - * - * @param string $url URL to call. - * @return string Response body. - * - * @throws FacturapiException - */ - protected function executeGetRequest($url): string - { - return $this->executeRequest('GET', $url); - } - - /** - * Executes HTTP POST request - * - * @param string $url URL to call. - * @param mixed $body Request body. - * @param bool $formenc Whether to send as form-urlencoded. - * @return string Response body. - * - * @throws FacturapiException - */ - protected function executePostRequest($url, $body, $formenc = false): string - { - $headers = array(); - $payload = null; - - if ($formenc) { - $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 string $url URL to call. - * @param array|null $body Request body. - * @return string Response body. - * - * @throws FacturapiException - */ - protected function executeJsonPostRequest($url, $body = null): string - { - $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 string $url URL to call. - * @param array|null $body Request body. - * - * @return string Response body. - * - * @throws FacturapiException - */ - protected function executeJsonPutRequest($url, $body): string - { - $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 string $url URL to call. - * @param array|string $body Multipart payload definition. - * - * @return string Response body. - * - * @throws FacturapiException - */ - protected function executeDataPutRequest($url, $body): string - { - $openStreams = array(); - - if (is_array($body)) { - if (!isset($body['cerFile'], $body['keyFile'], $body['password'])) { - throw new FacturapiException('Invalid certificate payload. Expected cerFile, keyFile and password.'); - } - - $cerStream = $this->openFileStream($body['cerFile']); - $keyStream = $this->openFileStream($body['keyFile']); - $openStreams[] = $cerStream; - $openStreams[] = $keyStream; - - $multipart = new MultipartStream(array( - array( - 'name' => 'cer', - 'contents' => Utils::streamFor($cerStream), - 'filename' => basename($body['cerFile']) - ), - array( - 'name' => 'key', - 'contents' => Utils::streamFor($keyStream), - 'filename' => basename($body['keyFile']) - ), - array( - 'name' => 'password', - 'contents' => (string) $body['password'] - ), - )); - } else { - $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 string $url URL to call. - * @param string|null $body Request body. - * - * @return string Response body. - * - * @throws FacturapiException - */ - protected function executeDeleteRequest($url, $body): string - { - $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|null $params Parameters (name => value). - * @return string URL query string. - */ - protected function arrayToParams($params): string - { - if ($params == null) { - return ''; - } - - $parts = array(); - foreach ($params as $parameter => $value) { - $this->appendQueryParam($parts, (string) $parameter, $value); - } - - return empty($parts) ? '' : '?' . implode('&', $parts); - } - - /** - * Appends one query parameter using the encoding the API documents and the - * other SDKs send: lists repeat the key (`status=a&status=b`), associative - * arrays use bracket notation (`date[gte]=...`) and `null` values are - * omitted instead of being sent empty. - * - * @param array $parts Accumulated `key=value` pairs. - * @param string $parameter Query parameter name. - * @param mixed $value Value to serialize. - * @return void - */ - private function appendQueryParam(array &$parts, string $parameter, $value): void - { - if ($value === null) { - return; - } - - if (is_array($value)) { - if (array_is_list($value)) { - foreach ($value as $item) { - $this->appendQueryParam($parts, $parameter, $item); - } - - return; - } - - foreach ($value as $key => $sub_param) { - $this->appendQueryParam($parts, $parameter . '[' . $key . ']', $sub_param); - } - - return; - } - - if (is_bool($value)) { - $parts[] = $parameter . '=' . ($value ? 'true' : 'false'); - return; - } - - $parts[] = $parameter . '=' . urlencode((string) $value); - } - - /** - * 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, - $this->normalizeResponseHeaders($response->getHeaders()) - ); - } - - return $output; - } - - private function normalizeResponseHeaders(array $headers): array - { - $normalized = array(); - foreach ($headers as $name => $values) { - $normalized[strtolower($name)] = is_array($values) ? implode(', ', $values) : (string) $values; - } - return $normalized; - } - - /** - * Extracts a human-readable message from known API error shapes. - * - * @param array|null $errorData Decoded API error payload. - * @param string $rawBody Raw response body. - * @return string - */ - private function extractErrorMessage(?array $errorData, string $rawBody): string - { - if ($errorData === null) { - return $rawBody; - } - - if (isset($errorData['message']) && is_string($errorData['message'])) { - return $errorData['message']; - } - - if ( - isset($errorData['error']) && - is_array($errorData['error']) && - isset($errorData['error']['message']) && - is_string($errorData['error']['message']) - ) { - return $errorData['error']['message']; - } - - if ( - isset($errorData['errors']) && - is_array($errorData['errors']) && - isset($errorData['errors'][0]) && - is_array($errorData['errors'][0]) && - isset($errorData['errors'][0]['message']) && - is_string($errorData['errors'][0]['message']) - ) { - return $errorData['errors'][0]['message']; - } - - $encoded = json_encode($errorData); - return $encoded === false ? $rawBody : $encoded; - } - - /** - * Builds base headers for API requests. - * - * @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/Http/FacturapiHttpClient.php b/src/Http/FacturapiHttpClient.php new file mode 100644 index 0000000..e73dc56 --- /dev/null +++ b/src/Http/FacturapiHttpClient.php @@ -0,0 +1,255 @@ +apiKey = base64_encode($apiKey . ':'); + $this->apiVersion = $config['apiVersion'] ?? 'v2'; + + $this->client = $config['httpClient'] ?? Psr18ClientDiscovery::find(); + $this->requestFactory = Psr17FactoryDiscovery::findRequestFactory(); + $this->streamFactory = Psr17FactoryDiscovery::findStreamFactory(); + } + + public function getLastStatus(): ?int + { + return $this->lastStatus; + } + + /** + * @throws FacturapiException + */ + public function get(string $endpoint, ?array $params = null): array + { + $url = $this->buildUrl($endpoint, $params); + return $this->executeRequest('GET', $url); + } + + /** + * @throws FacturapiException + */ + public function post(string $endpoint, array $data = [], ?array $query = null): array + { + $url = $this->buildUrl($endpoint, $query); + $body = $data === [] ? '' : json_encode($data, JSON_THROW_ON_ERROR); + return $this->executeRequest('POST', $url, ['Content-Type' => 'application/json'], $body); + } + + /** + * @throws FacturapiException + */ + public function put(string $endpoint, array $data = [], ?array $query = null): array + { + $url = $this->buildUrl($endpoint, $query); + $body = $data === [] ? '' : json_encode($data, JSON_THROW_ON_ERROR); + return $this->executeRequest('PUT', $url, ['Content-Type' => 'application/json'], $body); + } + + /** + * @throws FacturapiException + */ + public function delete(string $endpoint, ?array $query = null): array + { + $url = $this->buildUrl($endpoint, $query); + return $this->executeRequest('DELETE', $url); + } + + /** + * @throws FacturapiException + */ + /** + * @throws FacturapiException + */ + public function multipartRequest(string $method, string $endpoint, array $files, array $data = []): array + { + $url = $this->buildUrl($endpoint); + $builder = new MultipartStreamBuilder($this->streamFactory); + + foreach ($files as $name => $path) { + if (!file_exists($path) || !is_readable($path)) { + throw new FacturapiException('Unable to open file: ' . $path); + } + $stream = fopen($path, 'rb'); + if ($stream === false) { + throw new FacturapiException('Unable to open file: ' . $path); + } + $builder->addResource($name, $stream, ['filename' => basename($path)]); + } + + foreach ($data as $name => $value) { + $builder->addResource((string)$name, (string)$value); + } + + $multipartStream = $builder->build(); + $boundary = $builder->getBoundary(); + + return $this->executeRequest( + $method, + $url, + ['Content-Type' => 'multipart/form-data; boundary=' . $boundary], + (string)$multipartStream + ); + } + + /** + * @throws FacturapiException + */ + public function multipartPost(string $endpoint, array $files, array $data = []): array + { + return $this->multipartRequest('POST', $endpoint, $files, $data); + } + + /** + * @throws FacturapiException + */ + public function download(string $endpoint, ?array $params = null): string + { + $url = $this->buildUrl($endpoint, $params); + return $this->executeRawRequest('GET', $url); + } + + /** + * @throws FacturapiException + */ + public function postRaw(string $endpoint, array $data = [], ?array $query = null): string + { + $url = $this->buildUrl($endpoint, $query); + $body = $data === [] ? '' : json_encode($data, JSON_THROW_ON_ERROR); + return $this->executeRawRequest('POST', $url, ['Content-Type' => 'application/json'], $body); + } + + private function buildUrl(string $endpoint, ?array $params = null): string + { + $url = $this->baseUrl . $this->apiVersion . '/' . $endpoint; + if ($params !== null && $params !== []) { + foreach ($params as $k => $v) { + if (is_bool($v)) { + $params[$k] = $v ? 'true' : 'false'; + } + } + $url .= '?' . preg_replace('/%5B[0-9]+%5D/simU', '', http_build_query($params)); + } + return $url; + } + + /** + * @throws FacturapiException + * @throws \JsonException + */ + private function executeRequest(string $method, string $url, array $headers = [], string $body = ''): array + { + $rawResponse = $this->executeRawRequest($method, $url, $headers, $body); + if ($rawResponse === '') { + return []; + } + $decoded = json_decode($rawResponse, true, 512, JSON_THROW_ON_ERROR); + return is_array($decoded) ? $decoded : []; + } + + /** + * @throws FacturapiException + */ + private function executeRawRequest(string $method, string $url, array $headers = [], string $body = ''): string + { + $request = $this->requestFactory->createRequest($method, $url); + + $request = $request->withHeader('Authorization', 'Basic ' . $this->apiKey) + ->withHeader('User-Agent', $this->userAgent); + + foreach ($headers as $name => $value) { + $request = $request->withHeader($name, $value); + } + + if ($body !== '') { + $stream = $this->streamFactory->createStream($body); + $request = $request->withBody($stream); + } + + try { + $response = $this->client->sendRequest($request); + } catch (ClientExceptionInterface $e) { + throw new \Facturapi\Exceptions\NetworkException($e->getMessage(), 0, $e); + } + + $this->lastStatus = $response->getStatusCode(); + $output = (string) $response->getBody(); + + if ($this->lastStatus < 200 || $this->lastStatus > 299) { + $decoded = json_decode($output, true); + $errorData = is_array($decoded) ? $decoded : null; + $message = $this->extractErrorMessage($errorData, $output); + + $normalizedHeaders = []; + foreach ($response->getHeaders() as $name => $values) { + $normalizedHeaders[strtolower($name)] = implode(', ', $values); + } + + $exceptionClass = match (true) { + $this->lastStatus === 400, $this->lastStatus === 422 => \Facturapi\Exceptions\ValidationException::class, + $this->lastStatus === 401, $this->lastStatus === 403 => \Facturapi\Exceptions\AuthenticationException::class, + $this->lastStatus === 404 => \Facturapi\Exceptions\NotFoundException::class, + $this->lastStatus === 429 => \Facturapi\Exceptions\RateLimitException::class, + $this->lastStatus >= 500 => \Facturapi\Exceptions\ServerException::class, + default => FacturapiException::class, + }; + + throw new $exceptionClass( + $message, + 0, + null, + $errorData, + $this->lastStatus, + $output, + $normalizedHeaders + ); + } + + return $output; + } + + private function extractErrorMessage(?array $errorData, string $rawBody): string + { + if ($errorData === null) { + return $rawBody; + } + if (isset($errorData['message']) && is_string($errorData['message'])) { + return $errorData['message']; + } + if (isset($errorData['error']['message']) && is_string($errorData['error']['message'])) { + return $errorData['error']['message']; + } + if (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; + } +} diff --git a/src/InvoiceRelation.php b/src/InvoiceRelation.php deleted file mode 100644 index 6fd9166..0000000 --- a/src/InvoiceRelation.php +++ /dev/null @@ -1,15 +0,0 @@ - $this->rate, + 'base' => $this->base, + 'type' => $this->type, + 'factor' => $this->factor, + 'withholding' => $this->withholding, + ], fn (float|string|bool|null $v): bool => $v !== null); + } +} diff --git a/src/Models/CartaPorteAutotransporte.php b/src/Models/CartaPorteAutotransporte.php new file mode 100644 index 0000000..a36716b --- /dev/null +++ b/src/Models/CartaPorteAutotransporte.php @@ -0,0 +1,41 @@ + is_array($i) ? \Facturapi\Models\CartaPorteRemolque::fromArray($i) : $i, $data['Remolques']) : [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'PermSCT' => $this->PermSCT, + 'NumPermisoSCT' => $this->NumPermisoSCT, + 'IdentificacionVehicular' => $this->IdentificacionVehicular?->toArray(), + 'Seguros' => $this->Seguros?->toArray(), + 'Remolques' => $this->Remolques !== null ? array_map(fn (\Facturapi\Models\CartaPorteRemolque $i): array => $i->toArray(), $this->Remolques) : null + ], fn (string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/CartaPorteCantidadTransporta.php b/src/Models/CartaPorteCantidadTransporta.php new file mode 100644 index 0000000..545059c --- /dev/null +++ b/src/Models/CartaPorteCantidadTransporta.php @@ -0,0 +1,36 @@ + $this->Cantidad, + 'IDOrigen' => $this->IDOrigen, + 'IDDestino' => $this->IDDestino, + 'CvesTransporte' => $this->CvesTransporte + ], fn (float|string|null $v): bool => $v !== null); + } +} diff --git a/src/Models/CartaPorteCarroFerroviario.php b/src/Models/CartaPorteCarroFerroviario.php new file mode 100644 index 0000000..2e65690 --- /dev/null +++ b/src/Models/CartaPorteCarroFerroviario.php @@ -0,0 +1,41 @@ + is_array($i) ? \Facturapi\Models\CartaPorteContenedorFerroviario::fromArray($i) : $i, $data['Contenedor']) : [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'TipoCarro' => $this->TipoCarro, + 'MatriculaCarro' => $this->MatriculaCarro, + 'GuiaCarro' => $this->GuiaCarro, + 'ToneladasNetasCarro' => $this->ToneladasNetasCarro, + 'Contenedor' => $this->Contenedor !== null ? array_map(fn (\Facturapi\Models\CartaPorteContenedorFerroviario $i): array => $i->toArray(), $this->Contenedor) : null + ], fn (string|float|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/CartaPorteContenedorFerroviario.php b/src/Models/CartaPorteContenedorFerroviario.php new file mode 100644 index 0000000..b1e1396 --- /dev/null +++ b/src/Models/CartaPorteContenedorFerroviario.php @@ -0,0 +1,33 @@ + $this->TipoContenedor, + 'PesoContenedorVacio' => $this->PesoContenedorVacio, + 'PesoNetoMercancia' => $this->PesoNetoMercancia + ], fn (string|float|null $v): bool => $v !== null); + } +} diff --git a/src/Models/CartaPorteContenedorMaritimo.php b/src/Models/CartaPorteContenedorMaritimo.php new file mode 100644 index 0000000..ab42cd6 --- /dev/null +++ b/src/Models/CartaPorteContenedorMaritimo.php @@ -0,0 +1,45 @@ + $this->TipoContenedor, + 'MatriculaContenedor' => $this->MatriculaContenedor, + 'NumPrecinto' => $this->NumPrecinto, + 'IdCCPRelacionado' => $this->IdCCPRelacionado, + 'PlacaVMCCP' => $this->PlacaVMCCP, + 'FechaCertificacionCCP' => $this->FechaCertificacionCCP, + 'RemolquesCCP' => $this->RemolquesCCP + ], fn (string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/CartaPorteDataInput.php b/src/Models/CartaPorteDataInput.php new file mode 100644 index 0000000..b57cee5 --- /dev/null +++ b/src/Models/CartaPorteDataInput.php @@ -0,0 +1,63 @@ + $this->IdCCP, + 'TranspInternac' => $this->TranspInternac, + 'EntradaSalidaMerc' => $this->EntradaSalidaMerc, + 'PaisOrigenDestino' => $this->PaisOrigenDestino, + 'ViaEntradaSalida' => $this->ViaEntradaSalida, + 'TotalDistRec' => $this->TotalDistRec, + 'RegistroISTMO' => $this->RegistroISTMO, + 'UbicacionPoloOrigen' => $this->UbicacionPoloOrigen, + 'UbicacionPoloDestino' => $this->UbicacionPoloDestino, + 'RegimenesAduaneros' => $this->RegimenesAduaneros, + 'Ubicaciones' => $this->Ubicaciones, + 'Mercancias' => $this->Mercancias?->toArray(), + 'FiguraTransporte' => $this->FiguraTransporte + ], fn (float|string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/CartaPorteDataProperties.php b/src/Models/CartaPorteDataProperties.php new file mode 100644 index 0000000..c3211ac --- /dev/null +++ b/src/Models/CartaPorteDataProperties.php @@ -0,0 +1,63 @@ + $this->IdCCP, + 'TranspInternac' => $this->TranspInternac, + 'EntradaSalidaMerc' => $this->EntradaSalidaMerc, + 'PaisOrigenDestino' => $this->PaisOrigenDestino, + 'ViaEntradaSalida' => $this->ViaEntradaSalida, + 'TotalDistRec' => $this->TotalDistRec, + 'RegistroISTMO' => $this->RegistroISTMO, + 'UbicacionPoloOrigen' => $this->UbicacionPoloOrigen, + 'UbicacionPoloDestino' => $this->UbicacionPoloDestino, + 'RegimenesAduaneros' => $this->RegimenesAduaneros, + 'Ubicaciones' => $this->Ubicaciones, + 'Mercancias' => $this->Mercancias?->toArray(), + 'FiguraTransporte' => $this->FiguraTransporte + ], fn (float|string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/CartaPorteDerechosDePaso.php b/src/Models/CartaPorteDerechosDePaso.php new file mode 100644 index 0000000..aec828d --- /dev/null +++ b/src/Models/CartaPorteDerechosDePaso.php @@ -0,0 +1,30 @@ + $this->TipoDerechoDePaso, + 'KilometrajePagado' => $this->KilometrajePagado + ], fn (string|float|null $v): bool => $v !== null); + } +} diff --git a/src/Models/CartaPorteDetalleMercancia.php b/src/Models/CartaPorteDetalleMercancia.php new file mode 100644 index 0000000..7ba01cf --- /dev/null +++ b/src/Models/CartaPorteDetalleMercancia.php @@ -0,0 +1,39 @@ + $this->UnidadPesoMerc, + 'PesoBruto' => $this->PesoBruto, + 'PesoNeto' => $this->PesoNeto, + 'PesoTara' => $this->PesoTara, + 'NumPiezas' => $this->NumPiezas + ], fn (string|float|null $v): bool => $v !== null); + } +} diff --git a/src/Models/CartaPorteDocumentacionAduanera.php b/src/Models/CartaPorteDocumentacionAduanera.php new file mode 100644 index 0000000..348293e --- /dev/null +++ b/src/Models/CartaPorteDocumentacionAduanera.php @@ -0,0 +1,36 @@ + $this->TipoDocumento, + 'NumPedimento' => $this->NumPedimento, + 'IdentDocAduanero' => $this->IdentDocAduanero, + 'RFCImpo' => $this->RFCImpo + ], fn (?string $v): bool => $v !== null); + } +} diff --git a/src/Models/CartaPorteDomicilio.php b/src/Models/CartaPorteDomicilio.php new file mode 100644 index 0000000..982ad45 --- /dev/null +++ b/src/Models/CartaPorteDomicilio.php @@ -0,0 +1,54 @@ + $this->Calle, + 'NumeroExterior' => $this->NumeroExterior, + 'NumeroInterior' => $this->NumeroInterior, + 'Colonia' => $this->Colonia, + 'Localidad' => $this->Localidad, + 'Referencia' => $this->Referencia, + 'Municipio' => $this->Municipio, + 'Estado' => $this->Estado, + 'Pais' => $this->Pais, + 'CodigoPostal' => $this->CodigoPostal + ], fn (?string $v): bool => $v !== null); + } +} diff --git a/src/Models/CartaPorteGuiaIdentificacion.php b/src/Models/CartaPorteGuiaIdentificacion.php new file mode 100644 index 0000000..7fb1ba1 --- /dev/null +++ b/src/Models/CartaPorteGuiaIdentificacion.php @@ -0,0 +1,33 @@ + $this->NumeroGuiaIdentificacion, + 'DescripGuiaIdentificacion' => $this->DescripGuiaIdentificacion, + 'PesoGuiaIdentificacion' => $this->PesoGuiaIdentificacion + ], fn (string|float|null $v): bool => $v !== null); + } +} diff --git a/src/Models/CartaPorteIdentificacionVehicular.php b/src/Models/CartaPorteIdentificacionVehicular.php new file mode 100644 index 0000000..92aa01e --- /dev/null +++ b/src/Models/CartaPorteIdentificacionVehicular.php @@ -0,0 +1,36 @@ + $this->ConfigVehicular, + 'PesoBrutoVehicular' => $this->PesoBrutoVehicular, + 'PlacaVM' => $this->PlacaVM, + 'AnioModeloVM' => $this->AnioModeloVM + ], fn (float|string|null $v): bool => $v !== null); + } +} diff --git a/src/Models/CartaPorteInput.php b/src/Models/CartaPorteInput.php new file mode 100644 index 0000000..921ebfe --- /dev/null +++ b/src/Models/CartaPorteInput.php @@ -0,0 +1,30 @@ + $this->type, + 'data' => $this->data?->toArray() + ], fn (string|array $v): true => $v !== null); + } +} diff --git a/src/Models/CartaPorteMercancia.php b/src/Models/CartaPorteMercancia.php new file mode 100644 index 0000000..bb8877d --- /dev/null +++ b/src/Models/CartaPorteMercancia.php @@ -0,0 +1,158 @@ + is_array($i) ? \Facturapi\Models\CartaPorteDocumentacionAduanera::fromArray($i) : $i, $data['DocumentacionAduanera']) : [], + isset($data['GuiasIdentificacion']) && is_array($data['GuiasIdentificacion']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\CartaPorteGuiaIdentificacion::fromArray($i) : $i, $data['GuiasIdentificacion']) : [], + isset($data['CantidadTransporta']) && is_array($data['CantidadTransporta']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\CartaPorteCantidadTransporta::fromArray($i) : $i, $data['CantidadTransporta']) : [], + isset($data['DetalleMercancia']) && is_array($data['DetalleMercancia']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\CartaPorteDetalleMercancia::fromArray($i) : $i, $data['DetalleMercancia']) : [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'BienesTransp' => $this->BienesTransp, + 'ClaveSTCC' => $this->ClaveSTCC, + 'Descripcion' => $this->Descripcion, + 'Cantidad' => $this->Cantidad, + 'ClaveUnidad' => $this->ClaveUnidad, + 'Unidad' => $this->Unidad, + 'Dimensiones' => $this->Dimensiones, + 'MaterialPeligroso' => $this->MaterialPeligroso, + 'CveMaterialPeligroso' => $this->CveMaterialPeligroso, + 'Embalaje' => $this->Embalaje, + 'DescripEmbalaje' => $this->DescripEmbalaje, + 'SectorCOFEPRIS' => $this->SectorCOFEPRIS, + 'NombreIngredienteActivo' => $this->NombreIngredienteActivo, + 'NomQuimico' => $this->NomQuimico, + 'DenominacionGenericaProd' => $this->DenominacionGenericaProd, + 'DenominacionDistintivaProd' => $this->DenominacionDistintivaProd, + 'Fabricante' => $this->Fabricante, + 'FechaCaducidad' => $this->FechaCaducidad, + 'LoteMedicamento' => $this->LoteMedicamento, + 'FormaFarmaceutica' => $this->FormaFarmaceutica, + 'CondicionesEspTransp' => $this->CondicionesEspTransp, + 'RegistroSanitarioFolioAutorizacion' => $this->RegistroSanitarioFolioAutorizacion, + 'PermisoImportacion' => $this->PermisoImportacion, + 'FolioImpoVUCEM' => $this->FolioImpoVUCEM, + 'NumCAS' => $this->NumCAS, + 'RazonSocialEmpImp' => $this->RazonSocialEmpImp, + 'NumRegSanPlagCOFEPRIS' => $this->NumRegSanPlagCOFEPRIS, + 'DatosFabricante' => $this->DatosFabricante, + 'DatosFormulador' => $this->DatosFormulador, + 'DatosMaquilador' => $this->DatosMaquilador, + 'UsoAutorizado' => $this->UsoAutorizado, + 'PesoEnKg' => $this->PesoEnKg, + 'ValorMercancia' => $this->ValorMercancia, + 'Moneda' => $this->Moneda, + 'FraccionArancelaria' => $this->FraccionArancelaria, + 'UUIDComercioExt' => $this->UUIDComercioExt, + 'TipoMateria' => $this->TipoMateria, + 'DescripcionMateria' => $this->DescripcionMateria, + 'DocumentacionAduanera' => $this->DocumentacionAduanera !== null ? array_map(fn (\Facturapi\Models\CartaPorteDocumentacionAduanera $i): array => $i->toArray(), $this->DocumentacionAduanera) : null, + 'GuiasIdentificacion' => $this->GuiasIdentificacion !== null ? array_map(fn (\Facturapi\Models\CartaPorteGuiaIdentificacion $i): array => $i->toArray(), $this->GuiasIdentificacion) : null, + 'CantidadTransporta' => $this->CantidadTransporta !== null ? array_map(fn (\Facturapi\Models\CartaPorteCantidadTransporta $i): array => $i->toArray(), $this->CantidadTransporta) : null, + 'DetalleMercancia' => $this->DetalleMercancia !== null ? array_map(fn (\Facturapi\Models\CartaPorteDetalleMercancia $i): array => $i->toArray(), $this->DetalleMercancia) : null + ], fn (string|float|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/CartaPorteMercancias.php b/src/Models/CartaPorteMercancias.php new file mode 100644 index 0000000..5b88e1e --- /dev/null +++ b/src/Models/CartaPorteMercancias.php @@ -0,0 +1,59 @@ + is_array($i) ? \Facturapi\Models\CartaPorteMercancia::fromArray($i) : $i, $data['Mercancia']) : [], + isset($data['Autotransporte']) && is_array($data['Autotransporte']) ? \Facturapi\Models\CartaPorteAutotransporte::fromArray($data['Autotransporte']) : null, + isset($data['TransporteMaritimo']) && is_array($data['TransporteMaritimo']) ? \Facturapi\Models\CartaPorteTransporteMaritimo::fromArray($data['TransporteMaritimo']) : null, + isset($data['TransporteAereo']) && is_array($data['TransporteAereo']) ? \Facturapi\Models\CartaPorteTransporteAereo::fromArray($data['TransporteAereo']) : null, + isset($data['TransporteFerroviario']) && is_array($data['TransporteFerroviario']) ? \Facturapi\Models\CartaPorteTransporteFerroviario::fromArray($data['TransporteFerroviario']) : null + ); + } + + public function toArray(): array + { + return array_filter([ + 'PesoBrutoTotal' => $this->PesoBrutoTotal, + 'UnidadPeso' => $this->UnidadPeso, + 'PesoNetoTotal' => $this->PesoNetoTotal, + 'NumTotalMercancias' => $this->NumTotalMercancias, + 'CargoPorTasacion' => $this->CargoPorTasacion, + 'LogisticaInversaRecoleccionDevolucion' => $this->LogisticaInversaRecoleccionDevolucion, + 'Mercancia' => $this->Mercancia !== null ? array_map(fn (\Facturapi\Models\CartaPorteMercancia $i): array => $i->toArray(), $this->Mercancia) : null, + 'Autotransporte' => $this->Autotransporte?->toArray(), + 'TransporteMaritimo' => $this->TransporteMaritimo?->toArray(), + 'TransporteAereo' => $this->TransporteAereo?->toArray(), + 'TransporteFerroviario' => $this->TransporteFerroviario?->toArray() + ], fn (float|string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/CartaPorteOrCustomComplementInput.php b/src/Models/CartaPorteOrCustomComplementInput.php new file mode 100644 index 0000000..f5e8aa2 --- /dev/null +++ b/src/Models/CartaPorteOrCustomComplementInput.php @@ -0,0 +1,27 @@ + $this->type + ], fn (string $v): true => $v !== null); + } +} diff --git a/src/Models/CartaPorteOrCustomComplementProperties.php b/src/Models/CartaPorteOrCustomComplementProperties.php new file mode 100644 index 0000000..e74f036 --- /dev/null +++ b/src/Models/CartaPorteOrCustomComplementProperties.php @@ -0,0 +1,27 @@ + $this->type + ], fn (?string $v): bool => $v !== null); + } +} diff --git a/src/Models/CartaPorteProperties.php b/src/Models/CartaPorteProperties.php new file mode 100644 index 0000000..5b0b805 --- /dev/null +++ b/src/Models/CartaPorteProperties.php @@ -0,0 +1,30 @@ + $this->type, + 'data' => $this->data?->toArray() + ], fn (string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/CartaPorteRemolque.php b/src/Models/CartaPorteRemolque.php new file mode 100644 index 0000000..0fed268 --- /dev/null +++ b/src/Models/CartaPorteRemolque.php @@ -0,0 +1,30 @@ + $this->SubTipoRem, + 'Placa' => $this->Placa + ], fn (?string $v): bool => $v !== null); + } +} diff --git a/src/Models/CartaPorteSeguros.php b/src/Models/CartaPorteSeguros.php new file mode 100644 index 0000000..d1a01ce --- /dev/null +++ b/src/Models/CartaPorteSeguros.php @@ -0,0 +1,45 @@ + $this->AseguraRespCivil, + 'PolizaRespCivil' => $this->PolizaRespCivil, + 'AseguraMedAmbiente' => $this->AseguraMedAmbiente, + 'PolizaMedAmbiente' => $this->PolizaMedAmbiente, + 'AseguraCarga' => $this->AseguraCarga, + 'PolizaCarga' => $this->PolizaCarga, + 'PrimaSeguro' => $this->PrimaSeguro + ], fn (string|float|null $v): bool => $v !== null); + } +} diff --git a/src/Models/CartaPorteTransporteAereo.php b/src/Models/CartaPorteTransporteAereo.php new file mode 100644 index 0000000..1670a5d --- /dev/null +++ b/src/Models/CartaPorteTransporteAereo.php @@ -0,0 +1,60 @@ + $this->PermSCT, + 'NumPermisoSCT' => $this->NumPermisoSCT, + 'MatriculaAeronave' => $this->MatriculaAeronave, + 'NombreAseg' => $this->NombreAseg, + 'NumPolizaSeguro' => $this->NumPolizaSeguro, + 'NumeroGuia' => $this->NumeroGuia, + 'LugarContrato' => $this->LugarContrato, + 'CodigoTransportista' => $this->CodigoTransportista, + 'RFCEmbarcador' => $this->RFCEmbarcador, + 'NumRegIdTribEmbarc' => $this->NumRegIdTribEmbarc, + 'ResidenciaFiscalEmbarc' => $this->ResidenciaFiscalEmbarc, + 'NombreEmbarcador' => $this->NombreEmbarcador + ], fn (?string $v): bool => $v !== null); + } +} diff --git a/src/Models/CartaPorteTransporteFerroviario.php b/src/Models/CartaPorteTransporteFerroviario.php new file mode 100644 index 0000000..ff0c973 --- /dev/null +++ b/src/Models/CartaPorteTransporteFerroviario.php @@ -0,0 +1,46 @@ + is_array($i) ? \Facturapi\Models\CartaPorteDerechosDePaso::fromArray($i) : $i, $data['DerechosDePaso']) : [], + isset($data['Carro']) && is_array($data['Carro']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\CartaPorteCarroFerroviario::fromArray($i) : $i, $data['Carro']) : [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'TipoDeServicio' => $this->TipoDeServicio, + 'TipoDeTrafico' => $this->TipoDeTrafico, + 'NombreAseg' => $this->NombreAseg, + 'NumPolizaSeguro' => $this->NumPolizaSeguro, + 'DerechosDePaso' => $this->DerechosDePaso !== null ? array_map(fn (\Facturapi\Models\CartaPorteDerechosDePaso $i): array => $i->toArray(), $this->DerechosDePaso) : null, + 'Carro' => $this->Carro !== null ? array_map(fn (\Facturapi\Models\CartaPorteCarroFerroviario $i): array => $i->toArray(), $this->Carro) : null + ], fn (string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/CartaPorteTransporteMaritimo.php b/src/Models/CartaPorteTransporteMaritimo.php new file mode 100644 index 0000000..dc6698c --- /dev/null +++ b/src/Models/CartaPorteTransporteMaritimo.php @@ -0,0 +1,95 @@ + is_array($i) ? \Facturapi\Models\CartaPorteContenedorMaritimo::fromArray($i) : $i, $data['Contenedor']) : [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'PermSCT' => $this->PermSCT, + 'NumPermisoSCT' => $this->NumPermisoSCT, + 'NombreAseg' => $this->NombreAseg, + 'NumPolizaSeguro' => $this->NumPolizaSeguro, + 'TipoEmbarcacion' => $this->TipoEmbarcacion, + 'Matricula' => $this->Matricula, + 'NumeroOMI' => $this->NumeroOMI, + 'AnioEmbarcacion' => $this->AnioEmbarcacion, + 'NombreEmbarc' => $this->NombreEmbarc, + 'NacionalidadEmbarc' => $this->NacionalidadEmbarc, + 'UnidadesDeArqBruto' => $this->UnidadesDeArqBruto, + 'TipoCarga' => $this->TipoCarga, + 'Eslora' => $this->Eslora, + 'Manga' => $this->Manga, + 'Calado' => $this->Calado, + 'Puntal' => $this->Puntal, + 'LineaNaviera' => $this->LineaNaviera, + 'NombreAgenteNaviero' => $this->NombreAgenteNaviero, + 'NumAutorizacionNaviero' => $this->NumAutorizacionNaviero, + 'NumViaje' => $this->NumViaje, + 'NumConocEmbarc' => $this->NumConocEmbarc, + 'PermisoTempNavegacion' => $this->PermisoTempNavegacion, + 'Contenedor' => $this->Contenedor !== null ? array_map(fn (\Facturapi\Models\CartaPorteContenedorMaritimo $i): array => $i->toArray(), $this->Contenedor) : null + ], fn (string|float|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/ComercioExteriorDataInput.php b/src/Models/ComercioExteriorDataInput.php new file mode 100644 index 0000000..17c025b --- /dev/null +++ b/src/Models/ComercioExteriorDataInput.php @@ -0,0 +1,69 @@ + $this->Version, + 'MotivoTraslado' => $this->MotivoTraslado, + 'ClaveDePedimento' => $this->ClaveDePedimento, + 'CertificadoOrigen' => $this->CertificadoOrigen, + 'NumCertificadoOrigen' => $this->NumCertificadoOrigen, + 'NumeroExportadorConfiable' => $this->NumeroExportadorConfiable, + 'Incoterm' => $this->Incoterm, + 'Observaciones' => $this->Observaciones, + 'TipoCambioUSD' => $this->TipoCambioUSD, + 'TotalUSD' => $this->TotalUSD, + 'Emisor' => $this->Emisor, + 'Propietario' => $this->Propietario, + 'Receptor' => $this->Receptor, + 'Destinatario' => $this->Destinatario, + 'Mercancias' => $this->Mercancias?->toArray() + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/ComercioExteriorDataProperties.php b/src/Models/ComercioExteriorDataProperties.php new file mode 100644 index 0000000..bab2901 --- /dev/null +++ b/src/Models/ComercioExteriorDataProperties.php @@ -0,0 +1,69 @@ + $this->Version, + 'MotivoTraslado' => $this->MotivoTraslado, + 'ClaveDePedimento' => $this->ClaveDePedimento, + 'CertificadoOrigen' => $this->CertificadoOrigen, + 'NumCertificadoOrigen' => $this->NumCertificadoOrigen, + 'NumeroExportadorConfiable' => $this->NumeroExportadorConfiable, + 'Incoterm' => $this->Incoterm, + 'Observaciones' => $this->Observaciones, + 'TipoCambioUSD' => $this->TipoCambioUSD, + 'TotalUSD' => $this->TotalUSD, + 'Emisor' => $this->Emisor, + 'Propietario' => $this->Propietario, + 'Receptor' => $this->Receptor, + 'Destinatario' => $this->Destinatario, + 'Mercancias' => $this->Mercancias?->toArray() + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/ComercioExteriorDescripcionesEspecificas.php b/src/Models/ComercioExteriorDescripcionesEspecificas.php new file mode 100644 index 0000000..d905004 --- /dev/null +++ b/src/Models/ComercioExteriorDescripcionesEspecificas.php @@ -0,0 +1,36 @@ + $this->Marca, + 'Modelo' => $this->Modelo, + 'SubModelo' => $this->SubModelo, + 'NumeroSerie' => $this->NumeroSerie + ], fn (?string $v): bool => $v !== null); + } +} diff --git a/src/Models/ComercioExteriorDestinatario.php b/src/Models/ComercioExteriorDestinatario.php new file mode 100644 index 0000000..fb29d6f --- /dev/null +++ b/src/Models/ComercioExteriorDestinatario.php @@ -0,0 +1,35 @@ + is_array($i) ? \Facturapi\Models\ComercioExteriorDomicilio::fromArray($i) : $i, $data['Domicilio']) : [], + $data['NumRegIdTrib'] ?? null, + $data['Nombre'] ?? null + ); + } + + public function toArray(): array + { + return array_filter([ + 'Domicilio' => $this->Domicilio !== null ? array_map(fn (\Facturapi\Models\ComercioExteriorDomicilio $i): array => $i->toArray(), $this->Domicilio) : null, + 'NumRegIdTrib' => $this->NumRegIdTrib, + 'Nombre' => $this->Nombre + ], fn (string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/ComercioExteriorDomicilio.php b/src/Models/ComercioExteriorDomicilio.php new file mode 100644 index 0000000..d0f06c3 --- /dev/null +++ b/src/Models/ComercioExteriorDomicilio.php @@ -0,0 +1,54 @@ + $this->Calle, + 'NumeroExterior' => $this->NumeroExterior, + 'NumeroInterior' => $this->NumeroInterior, + 'Colonia' => $this->Colonia, + 'Localidad' => $this->Localidad, + 'Referencia' => $this->Referencia, + 'Municipio' => $this->Municipio, + 'Estado' => $this->Estado, + 'Pais' => $this->Pais, + 'CodigoPostal' => $this->CodigoPostal + ], fn (?string $v): bool => $v !== null); + } +} diff --git a/src/Models/ComercioExteriorEmisor.php b/src/Models/ComercioExteriorEmisor.php new file mode 100644 index 0000000..c057713 --- /dev/null +++ b/src/Models/ComercioExteriorEmisor.php @@ -0,0 +1,30 @@ + $this->Domicilio?->toArray(), + 'Curp' => $this->Curp + ], fn (string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/ComercioExteriorInput.php b/src/Models/ComercioExteriorInput.php new file mode 100644 index 0000000..914d207 --- /dev/null +++ b/src/Models/ComercioExteriorInput.php @@ -0,0 +1,30 @@ + $this->type, + 'data' => $this->data?->toArray() + ], fn (string|array $v): true => $v !== null); + } +} diff --git a/src/Models/ComercioExteriorMercancia.php b/src/Models/ComercioExteriorMercancia.php new file mode 100644 index 0000000..e8ff923 --- /dev/null +++ b/src/Models/ComercioExteriorMercancia.php @@ -0,0 +1,47 @@ + is_array($i) ? \Facturapi\Models\ComercioExteriorDescripcionesEspecificas::fromArray($i) : $i, $data['DescripcionesEspecificas']) : [], + $data['NoIdentificacion'] ?? '', + $data['FraccionArancelaria'] ?? null, + $data['CantidadAduana'] ?? 0, + $data['UnidadAduana'] ?? null, + $data['ValorUnitarioAduana'] ?? 0, + $data['ValorDolares'] ?? 0 + ); + } + + public function toArray(): array + { + return array_filter([ + 'DescripcionesEspecificas' => $this->DescripcionesEspecificas !== null ? array_map(fn (\Facturapi\Models\ComercioExteriorDescripcionesEspecificas $i): array => $i->toArray(), $this->DescripcionesEspecificas) : null, + 'NoIdentificacion' => $this->NoIdentificacion, + 'FraccionArancelaria' => $this->FraccionArancelaria, + 'CantidadAduana' => $this->CantidadAduana, + 'UnidadAduana' => $this->UnidadAduana, + 'ValorUnitarioAduana' => $this->ValorUnitarioAduana, + 'ValorDolares' => $this->ValorDolares + ], fn (string|float|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/ComercioExteriorMercancias.php b/src/Models/ComercioExteriorMercancias.php new file mode 100644 index 0000000..ff396b5 --- /dev/null +++ b/src/Models/ComercioExteriorMercancias.php @@ -0,0 +1,29 @@ + is_array($i) ? \Facturapi\Models\ComercioExteriorMercancia::fromArray($i) : $i, $data['Mercancia']) : [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'Mercancia' => $this->Mercancia !== null ? array_map(fn (\Facturapi\Models\ComercioExteriorMercancia $i): array => $i->toArray(), $this->Mercancia) : null + ], fn (array $v): true => $v !== null); + } +} diff --git a/src/Models/ComercioExteriorProperties.php b/src/Models/ComercioExteriorProperties.php new file mode 100644 index 0000000..952d44c --- /dev/null +++ b/src/Models/ComercioExteriorProperties.php @@ -0,0 +1,30 @@ + $this->type, + 'data' => $this->data?->toArray() + ], fn (string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/ComercioExteriorPropietario.php b/src/Models/ComercioExteriorPropietario.php new file mode 100644 index 0000000..8db8e63 --- /dev/null +++ b/src/Models/ComercioExteriorPropietario.php @@ -0,0 +1,30 @@ + $this->NumRegIdTrib, + 'ResidenciaFiscal' => $this->ResidenciaFiscal + ], fn (string $v): true => $v !== null); + } +} diff --git a/src/Models/ComercioExteriorReceptor.php b/src/Models/ComercioExteriorReceptor.php new file mode 100644 index 0000000..1f986de --- /dev/null +++ b/src/Models/ComercioExteriorReceptor.php @@ -0,0 +1,30 @@ + $this->Domicilio?->toArray(), + 'NumRegIdTrib' => $this->NumRegIdTrib + ], fn (string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/CommonAddressProperties.php b/src/Models/CommonAddressProperties.php new file mode 100644 index 0000000..2005961 --- /dev/null +++ b/src/Models/CommonAddressProperties.php @@ -0,0 +1,109 @@ + + */ +readonly class CommonAddressProperties implements ArrayAccess +{ + public function __construct( + public ?string $street = null, + public ?string $exterior = null, + public ?string $interior = null, + public ?string $neighborhood = null, + public ?string $city = null, + public ?string $municipality = null, + public ?string $zip = null, + public ?string $state = null, + public ?string $country = null, + ) { + } + + /** + * @param array $data + */ + public static function fromArray(array $data): self + { + return new self( + $data['street'] ?? null, + $data['exterior'] ?? null, + $data['interior'] ?? null, + $data['neighborhood'] ?? null, + $data['city'] ?? null, + $data['municipality'] ?? null, + $data['zip'] ?? null, + $data['state'] ?? null, + $data['country'] ?? null, + ); + } + + /** + * @return array + */ + public function toArray(): array + { + return array_filter([ + 'street' => $this->street, + 'exterior' => $this->exterior, + 'interior' => $this->interior, + 'neighborhood' => $this->neighborhood, + 'city' => $this->city, + 'municipality' => $this->municipality, + 'zip' => $this->zip, + 'state' => $this->state, + 'country' => $this->country, + ], fn (?string $v): bool => $v !== null); + } + + public function offsetExists(mixed $offset): bool + { + return is_string($offset) && match ($offset) { + 'street' => $this->street !== null, + 'exterior' => $this->exterior !== null, + 'interior' => $this->interior !== null, + 'neighborhood' => $this->neighborhood !== null, + 'city' => $this->city !== null, + 'municipality' => $this->municipality !== null, + 'zip' => $this->zip !== null, + 'state' => $this->state !== null, + 'country' => $this->country !== null, + default => false, + }; + } + + public function offsetGet(mixed $offset): ?string + { + if (!is_string($offset)) { + return null; + } + + return match ($offset) { + 'street' => $this->street, + 'exterior' => $this->exterior, + 'interior' => $this->interior, + 'neighborhood' => $this->neighborhood, + 'city' => $this->city, + 'municipality' => $this->municipality, + 'zip' => $this->zip, + 'state' => $this->state, + 'country' => $this->country, + default => null, + }; + } + + public function offsetSet(mixed $offset, mixed $value): void + { + throw new BadMethodCallException('Cannot modify readonly address properties.'); + } + + public function offsetUnset(mixed $offset): void + { + throw new BadMethodCallException('Cannot modify readonly address properties.'); + } +} diff --git a/src/Models/CustomComplementInput.php b/src/Models/CustomComplementInput.php new file mode 100644 index 0000000..a721125 --- /dev/null +++ b/src/Models/CustomComplementInput.php @@ -0,0 +1,30 @@ + $this->type, + 'data' => $this->data?->toArray() + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/CustomComplementProperties.php b/src/Models/CustomComplementProperties.php new file mode 100644 index 0000000..0ab6d14 --- /dev/null +++ b/src/Models/CustomComplementProperties.php @@ -0,0 +1,30 @@ + $this->type, + 'data' => $this->data?->toArray() + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/Customer.php b/src/Models/Customer.php new file mode 100644 index 0000000..feb4d19 --- /dev/null +++ b/src/Models/Customer.php @@ -0,0 +1,78 @@ + $data + */ + public static function fromArray(array $data): self + { + $address = null; + if (isset($data['address'])) { + if ($data['address'] instanceof CommonAddressProperties) { + $address = $data['address']; + } elseif (is_array($data['address'])) { + $address = CommonAddressProperties::fromArray($data['address']); + } + } + + return new self( + $data['id'] ?? '', + $data['created_at'] ?? '', + $data['livemode'] ?? false, + $data['edit_link'] ?? null, + $data['edit_link_expires_at'] ?? null, + $data['sat_validated_at'] ?? null, + $data['legal_name'] ?? null, + $data['tax_id'] ?? null, + $data['tax_system'] ?? null, + $data['email'] ?? null, + $data['phone'] ?? null, + $data['default_invoice_use'] ?? null, + $address + ); + } + + /** + * @return array + */ + public function toArray(): array + { + return array_filter([ + 'id' => $this->id, + 'created_at' => $this->created_at, + 'livemode' => $this->livemode, + 'edit_link' => $this->edit_link, + 'edit_link_expires_at' => $this->edit_link_expires_at, + 'sat_validated_at' => $this->sat_validated_at, + 'legal_name' => $this->legal_name, + 'tax_id' => $this->tax_id, + 'tax_system' => $this->tax_system, + 'email' => $this->email, + 'phone' => $this->phone, + 'default_invoice_use' => $this->default_invoice_use, + 'address' => $this->address?->toArray(), + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/CustomerComercioExterior.php b/src/Models/CustomerComercioExterior.php new file mode 100644 index 0000000..d1fa8fc --- /dev/null +++ b/src/Models/CustomerComercioExterior.php @@ -0,0 +1,27 @@ + $this->id + ], fn (?string $v): bool => $v !== null); + } +} diff --git a/src/Models/CustomerCommonProperties.php b/src/Models/CustomerCommonProperties.php new file mode 100644 index 0000000..af03c3d --- /dev/null +++ b/src/Models/CustomerCommonProperties.php @@ -0,0 +1,42 @@ + $this->legal_name, + 'tax_id' => $this->tax_id, + 'tax_system' => $this->tax_system, + 'email' => $this->email, + 'phone' => $this->phone, + 'default_invoice_use' => $this->default_invoice_use + ], fn (?string $v): bool => $v !== null); + } +} diff --git a/src/Models/CustomerCreateInput.php b/src/Models/CustomerCreateInput.php new file mode 100644 index 0000000..cdc10c8 --- /dev/null +++ b/src/Models/CustomerCreateInput.php @@ -0,0 +1,45 @@ + $this->legal_name, + 'tax_id' => $this->tax_id, + 'tax_system' => $this->tax_system, + 'email' => $this->email, + 'phone' => $this->phone, + 'default_invoice_use' => $this->default_invoice_use, + 'address' => $this->address + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/CustomerInfo.php b/src/Models/CustomerInfo.php new file mode 100644 index 0000000..8875308 --- /dev/null +++ b/src/Models/CustomerInfo.php @@ -0,0 +1,36 @@ + $this->id, + 'legal_name' => $this->legal_name, + 'tax_id' => $this->tax_id, + 'address' => $this->address + ], fn (string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/CustomerNonEditableProperties.php b/src/Models/CustomerNonEditableProperties.php new file mode 100644 index 0000000..3360faf --- /dev/null +++ b/src/Models/CustomerNonEditableProperties.php @@ -0,0 +1,33 @@ + $this->edit_link, + 'edit_link_expires_at' => $this->edit_link_expires_at, + 'sat_validated_at' => $this->sat_validated_at + ], fn (?string $v): bool => $v !== null); + } +} diff --git a/src/Models/CustomerProperties.php b/src/Models/CustomerProperties.php new file mode 100644 index 0000000..67a3265 --- /dev/null +++ b/src/Models/CustomerProperties.php @@ -0,0 +1,45 @@ + $this->legal_name, + 'tax_id' => $this->tax_id, + 'tax_system' => $this->tax_system, + 'email' => $this->email, + 'phone' => $this->phone, + 'default_invoice_use' => $this->default_invoice_use, + 'address' => $this->address + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/CustomerSearchResult.php b/src/Models/CustomerSearchResult.php new file mode 100644 index 0000000..7ed72cf --- /dev/null +++ b/src/Models/CustomerSearchResult.php @@ -0,0 +1,47 @@ + is_array($i) ? \Facturapi\Models\Customer::fromArray($i) : $i, $data['data']) : [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'page' => $this->page, + 'total_pages' => $this->total_pages, + 'total_results' => $this->total_results, + 'previous_cursor' => $this->previous_cursor, + 'next_cursor' => $this->next_cursor, + 'totals_are_capped' => $this->totals_are_capped, + 'data' => $this->data !== null ? array_map(fn (\Facturapi\Models\Customer $i): array => $i->toArray(), $this->data) : null + ], fn (int|string|bool|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/DateRange.php b/src/Models/DateRange.php new file mode 100644 index 0000000..c6ec813 --- /dev/null +++ b/src/Models/DateRange.php @@ -0,0 +1,36 @@ + $this->gt, + 'gte' => $this->gte, + 'lt' => $this->lt, + 'lte' => $this->lte + ], fn (?string $v): bool => $v !== null); + } +} diff --git a/src/Models/DomainAvailability.php b/src/Models/DomainAvailability.php new file mode 100644 index 0000000..d219d9a --- /dev/null +++ b/src/Models/DomainAvailability.php @@ -0,0 +1,27 @@ + $this->available + ]; + } +} diff --git a/src/Models/ErrorDetail.php b/src/Models/ErrorDetail.php new file mode 100644 index 0000000..813572a --- /dev/null +++ b/src/Models/ErrorDetail.php @@ -0,0 +1,39 @@ + $this->message, + 'code' => $this->code, + 'location' => $this->location, + 'path' => $this->path, + 'source' => $this->source + ], fn (?string $v): bool => $v !== null); + } +} diff --git a/src/Models/ErrorMessage.php b/src/Models/ErrorMessage.php new file mode 100644 index 0000000..7101094 --- /dev/null +++ b/src/Models/ErrorMessage.php @@ -0,0 +1,27 @@ + $this->message + ], fn (string $v): true => $v !== null); + } +} diff --git a/src/Models/EventBase.php b/src/Models/EventBase.php new file mode 100644 index 0000000..2c8178a --- /dev/null +++ b/src/Models/EventBase.php @@ -0,0 +1,36 @@ + $this->id, + 'created_at' => $this->created_at, + 'livemode' => $this->livemode, + 'organization' => $this->organization + ], fn (bool|string|null $v): bool => $v !== null); + } +} diff --git a/src/Models/GenericError.php b/src/Models/GenericError.php new file mode 100644 index 0000000..54bb039 --- /dev/null +++ b/src/Models/GenericError.php @@ -0,0 +1,47 @@ + is_array($i) ? \Facturapi\Models\ErrorDetail::fromArray($i) : $i, $data['errors']) : [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'message' => $this->message, + 'status' => $this->status, + 'ok' => $this->ok, + 'code' => $this->code, + 'location' => $this->location, + 'path' => $this->path, + 'errors' => $this->errors !== null ? array_map(fn (\Facturapi\Models\ErrorDetail $i): array => $i->toArray(), $this->errors) : null + ], fn (int|bool|string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/GlobalInvoiceInput.php b/src/Models/GlobalInvoiceInput.php new file mode 100644 index 0000000..2f69120 --- /dev/null +++ b/src/Models/GlobalInvoiceInput.php @@ -0,0 +1,54 @@ + $this->from, + 'to' => $this->to, + 'periodicity' => $this->periodicity, + 'months' => $this->months, + 'folio_number' => $this->folio_number, + 'series' => $this->series, + 'date' => $this->date, + 'payment_form' => $this->payment_form, + 'receipts' => $this->receipts, + 'limit_to_max_receipts' => $this->limit_to_max_receipts + ], fn (int|string|bool|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/HidroYPetroComplementInput.php b/src/Models/HidroYPetroComplementInput.php new file mode 100644 index 0000000..29ae2c4 --- /dev/null +++ b/src/Models/HidroYPetroComplementInput.php @@ -0,0 +1,36 @@ + $this->tipo_permiso, + 'numero_permiso' => $this->numero_permiso, + 'sub_producto_hyp' => $this->sub_producto_hyp, + 'clave_hyp' => $this->clave_hyp + ], fn (?string $v): bool => $v !== null); + } +} diff --git a/src/Models/IeduComplementInput.php b/src/Models/IeduComplementInput.php new file mode 100644 index 0000000..fd0ad64 --- /dev/null +++ b/src/Models/IeduComplementInput.php @@ -0,0 +1,39 @@ + $this->nombreAlumno, + 'CURP' => $this->CURP, + 'nivelEducativo' => $this->nivelEducativo, + 'autRVOE' => $this->autRVOE, + 'rfcPago' => $this->rfcPago + ], fn (?string $v): bool => $v !== null); + } +} diff --git a/src/Models/IepsTax.php b/src/Models/IepsTax.php new file mode 100644 index 0000000..8ee3716 --- /dev/null +++ b/src/Models/IepsTax.php @@ -0,0 +1,43 @@ + $this->rate, + 'base' => $this->base, + 'type' => $this->type, + 'factor' => $this->factor, + 'withholding' => $this->withholding, + 'ieps_mode' => $this->ieps_mode, + ], fn (float|bool|string|null $v): bool => $v !== null); + } +} diff --git a/src/Models/Invoice.php b/src/Models/Invoice.php new file mode 100644 index 0000000..3cf8412 --- /dev/null +++ b/src/Models/Invoice.php @@ -0,0 +1,137 @@ + is_array($i) ? \Facturapi\Models\LineItem::fromArray($i) : $i, $data['items']) : [], + isset($data['related_documents']) && is_array($data['related_documents']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\RelatedDocument::fromArray($i) : $i, $data['related_documents']) : [], + $data['received_payment_ids'] ?? [], + $data['target_invoice_ids'] ?? [], + $data['currency'] ?? null, + $data['exchange'] ?? 0, + isset($data['complements']) && is_array($data['complements']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\NominaOrCustomComplementProperties::fromArray($i) : $i, $data['complements']) : [], + $data['pdf_custom_section'] ?? null, + $data['addenda'] ?? null, + isset($data['namespaces']) && is_array($data['namespaces']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\NamespaceProperties::fromArray($i) : $i, $data['namespaces']) : [], + isset($data['stamp']) && is_array($data['stamp']) ? \Facturapi\Models\Stamp::fromArray($data['stamp']) : null + ); + } + + public function toArray(): array + { + return array_filter([ + 'id' => $this->id, + 'created_at' => $this->created_at, + 'livemode' => $this->livemode, + 'status' => $this->status, + 'cancellation_status' => $this->cancellation_status, + 'canceled_at' => $this->canceled_at, + 'verification_url' => $this->verification_url, + 'date' => $this->date, + 'address' => $this->address, + 'type' => $this->type, + 'customer' => $this->customer?->toArray(), + 'total' => $this->total, + 'uuid' => $this->uuid, + 'folio_number' => $this->folio_number, + 'series' => $this->series, + 'external_id' => $this->external_id, + 'idempotency_key' => $this->idempotency_key, + 'payment_form' => $this->payment_form, + 'payment_method' => $this->payment_method, + 'use' => $this->use, + 'conditions' => $this->conditions, + 'total_payment_amount' => $this->total_payment_amount, + 'total_payment_amount_converted' => $this->total_payment_amount_converted, + 'is_ready_to_stamp' => $this->is_ready_to_stamp, + 'items' => $this->items !== null ? array_map(fn (\Facturapi\Models\LineItem $i): array => $i->toArray(), $this->items) : null, + 'related_documents' => $this->related_documents !== null ? array_map(fn (\Facturapi\Models\RelatedDocument $i): array => $i->toArray(), $this->related_documents) : null, + 'received_payment_ids' => $this->received_payment_ids, + 'target_invoice_ids' => $this->target_invoice_ids, + 'currency' => $this->currency, + 'exchange' => $this->exchange, + 'complements' => $this->complements !== null ? array_map(fn (\Facturapi\Models\NominaOrCustomComplementProperties $i): array => $i->toArray(), $this->complements) : null, + 'pdf_custom_section' => $this->pdf_custom_section, + 'addenda' => $this->addenda, + 'namespaces' => $this->namespaces !== null ? array_map(fn (\Facturapi\Models\NamespaceProperties $i): array => $i->toArray(), $this->namespaces) : null, + 'stamp' => $this->stamp?->toArray() + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/InvoiceCommonEditInputProperties.php b/src/Models/InvoiceCommonEditInputProperties.php new file mode 100644 index 0000000..b30967d --- /dev/null +++ b/src/Models/InvoiceCommonEditInputProperties.php @@ -0,0 +1,60 @@ + $this->customer, + 'status' => $this->status, + 'date' => $this->date, + 'address' => $this->address, + 'external_id' => $this->external_id, + 'idempotency_key' => $this->idempotency_key, + 'folio_number' => $this->folio_number, + 'series' => $this->series, + 'pdf_custom_section' => $this->pdf_custom_section, + 'addenda' => $this->addenda, + 'namespaces' => $this->namespaces, + 'pdf_options' => $this->pdf_options + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/InvoiceCommonInputProperties.php b/src/Models/InvoiceCommonInputProperties.php new file mode 100644 index 0000000..d2eaf68 --- /dev/null +++ b/src/Models/InvoiceCommonInputProperties.php @@ -0,0 +1,60 @@ + $this->customer, + 'status' => $this->status, + 'date' => $this->date, + 'address' => $this->address, + 'external_id' => $this->external_id, + 'idempotency_key' => $this->idempotency_key, + 'folio_number' => $this->folio_number, + 'series' => $this->series, + 'pdf_custom_section' => $this->pdf_custom_section, + 'addenda' => $this->addenda, + 'namespaces' => $this->namespaces, + 'pdf_options' => $this->pdf_options + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/InvoiceDraft.php b/src/Models/InvoiceDraft.php new file mode 100644 index 0000000..f7aac2b --- /dev/null +++ b/src/Models/InvoiceDraft.php @@ -0,0 +1,113 @@ + is_array($i) ? \Facturapi\Models\LineItem::fromArray($i) : $i, $data['items']) : [], + isset($data['related_documents']) && is_array($data['related_documents']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\RelatedDocument::fromArray($i) : $i, $data['related_documents']) : [], + $data['currency'] ?? null, + $data['exchange'] ?? 0, + isset($data['complements']) && is_array($data['complements']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\NominaOrCustomComplementProperties::fromArray($i) : $i, $data['complements']) : [], + $data['pdf_custom_section'] ?? null, + $data['addenda'] ?? null, + isset($data['namespaces']) && is_array($data['namespaces']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\NamespaceProperties::fromArray($i) : $i, $data['namespaces']) : [], + $data['is_ready_to_stamp'] ?? false, + $data['stamp'] ?? null + ); + } + + public function toArray(): array + { + return array_filter([ + 'id' => $this->id, + 'created_at' => $this->created_at, + 'livemode' => $this->livemode, + 'status' => $this->status, + 'cancellation_status' => $this->cancellation_status, + 'verification_url' => $this->verification_url, + 'date' => $this->date, + 'address' => $this->address, + 'type' => $this->type, + 'customer' => $this->customer?->toArray(), + 'total' => $this->total, + 'uuid' => $this->uuid, + 'folio_number' => $this->folio_number, + 'series' => $this->series, + 'external_id' => $this->external_id, + 'idempotency_key' => $this->idempotency_key, + 'payment_form' => $this->payment_form, + 'items' => $this->items !== null ? array_map(fn (\Facturapi\Models\LineItem $i): array => $i->toArray(), $this->items) : null, + 'related_documents' => $this->related_documents !== null ? array_map(fn (\Facturapi\Models\RelatedDocument $i): array => $i->toArray(), $this->related_documents) : null, + 'currency' => $this->currency, + 'exchange' => $this->exchange, + 'complements' => $this->complements !== null ? array_map(fn (\Facturapi\Models\NominaOrCustomComplementProperties $i): array => $i->toArray(), $this->complements) : null, + 'pdf_custom_section' => $this->pdf_custom_section, + 'addenda' => $this->addenda, + 'namespaces' => $this->namespaces !== null ? array_map(fn (\Facturapi\Models\NamespaceProperties $i): array => $i->toArray(), $this->namespaces) : null, + 'is_ready_to_stamp' => $this->is_ready_to_stamp, + 'stamp' => $this->stamp + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/InvoiceDraftProperties.php b/src/Models/InvoiceDraftProperties.php new file mode 100644 index 0000000..4084278 --- /dev/null +++ b/src/Models/InvoiceDraftProperties.php @@ -0,0 +1,104 @@ + is_array($i) ? \Facturapi\Models\LineItem::fromArray($i) : $i, $data['items']) : [], + isset($data['related_documents']) && is_array($data['related_documents']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\RelatedDocument::fromArray($i) : $i, $data['related_documents']) : [], + $data['currency'] ?? null, + $data['exchange'] ?? 0, + isset($data['complements']) && is_array($data['complements']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\NominaOrCustomComplementProperties::fromArray($i) : $i, $data['complements']) : [], + $data['pdf_custom_section'] ?? null, + $data['addenda'] ?? null, + isset($data['namespaces']) && is_array($data['namespaces']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\NamespaceProperties::fromArray($i) : $i, $data['namespaces']) : [], + $data['is_ready_to_stamp'] ?? false, + $data['stamp'] ?? null + ); + } + + public function toArray(): array + { + return array_filter([ + 'status' => $this->status, + 'cancellation_status' => $this->cancellation_status, + 'verification_url' => $this->verification_url, + 'date' => $this->date, + 'address' => $this->address, + 'type' => $this->type, + 'customer' => $this->customer?->toArray(), + 'total' => $this->total, + 'uuid' => $this->uuid, + 'folio_number' => $this->folio_number, + 'series' => $this->series, + 'external_id' => $this->external_id, + 'idempotency_key' => $this->idempotency_key, + 'payment_form' => $this->payment_form, + 'items' => $this->items !== null ? array_map(fn (\Facturapi\Models\LineItem $i): array => $i->toArray(), $this->items) : null, + 'related_documents' => $this->related_documents !== null ? array_map(fn (\Facturapi\Models\RelatedDocument $i): array => $i->toArray(), $this->related_documents) : null, + 'currency' => $this->currency, + 'exchange' => $this->exchange, + 'complements' => $this->complements !== null ? array_map(fn (\Facturapi\Models\NominaOrCustomComplementProperties $i): array => $i->toArray(), $this->complements) : null, + 'pdf_custom_section' => $this->pdf_custom_section, + 'addenda' => $this->addenda, + 'namespaces' => $this->namespaces !== null ? array_map(fn (\Facturapi\Models\NamespaceProperties $i): array => $i->toArray(), $this->namespaces) : null, + 'is_ready_to_stamp' => $this->is_ready_to_stamp, + 'stamp' => $this->stamp + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/InvoiceEgresoEditInput.php b/src/Models/InvoiceEgresoEditInput.php new file mode 100644 index 0000000..49a3aea --- /dev/null +++ b/src/Models/InvoiceEgresoEditInput.php @@ -0,0 +1,93 @@ + is_array($i) ? \Facturapi\Models\RelatedDocumentInput::fromArray($i) : $i, $data['related_documents']) : [], + isset($data['items']) && is_array($data['items']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\LineItemEgresoInput::fromArray($i) : $i, $data['items']) : [], + $data['use'] ?? null, + $data['currency'] ?? null, + $data['exchange'] ?? 0, + isset($data['complements']) && is_array($data['complements']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\CustomComplementInput::fromArray($i) : $i, $data['complements']) : [], + $data['customer'] ?? null, + $data['status'] ?? null, + $data['date'] ?? null, + $data['address'] ?? null, + $data['external_id'] ?? null, + $data['idempotency_key'] ?? null, + $data['folio_number'] ?? 0, + $data['series'] ?? null, + $data['pdf_custom_section'] ?? null, + $data['addenda'] ?? null, + $data['namespaces'] ?? [], + $data['pdf_options'] ?? [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'type' => $this->type, + 'payment_form' => $this->payment_form, + 'payment_method' => $this->payment_method, + 'related_documents' => $this->related_documents !== null ? array_map(fn (\Facturapi\Models\RelatedDocumentInput $i): array => $i->toArray(), $this->related_documents) : null, + 'items' => $this->items !== null ? array_map(fn (\Facturapi\Models\LineItemEgresoInput $i): array => $i->toArray(), $this->items) : null, + 'use' => $this->use, + 'currency' => $this->currency, + 'exchange' => $this->exchange, + 'complements' => $this->complements !== null ? array_map(fn (\Facturapi\Models\CustomComplementInput $i): array => $i->toArray(), $this->complements) : null, + 'customer' => $this->customer, + 'status' => $this->status, + 'date' => $this->date, + 'address' => $this->address, + 'external_id' => $this->external_id, + 'idempotency_key' => $this->idempotency_key, + 'folio_number' => $this->folio_number, + 'series' => $this->series, + 'pdf_custom_section' => $this->pdf_custom_section, + 'addenda' => $this->addenda, + 'namespaces' => $this->namespaces, + 'pdf_options' => $this->pdf_options + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/InvoiceEgresoInput.php b/src/Models/InvoiceEgresoInput.php new file mode 100644 index 0000000..4bf6a2b --- /dev/null +++ b/src/Models/InvoiceEgresoInput.php @@ -0,0 +1,93 @@ + is_array($i) ? \Facturapi\Models\RelatedDocumentInput::fromArray($i) : $i, $data['related_documents']) : [], + isset($data['items']) && is_array($data['items']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\LineItemEgresoInput::fromArray($i) : $i, $data['items']) : [], + $data['use'] ?? null, + $data['currency'] ?? null, + $data['exchange'] ?? 0, + isset($data['complements']) && is_array($data['complements']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\CustomComplementInput::fromArray($i) : $i, $data['complements']) : [], + $data['customer'] ?? '', + $data['status'] ?? null, + $data['date'] ?? null, + $data['address'] ?? null, + $data['external_id'] ?? null, + $data['idempotency_key'] ?? null, + $data['folio_number'] ?? 0, + $data['series'] ?? null, + $data['pdf_custom_section'] ?? null, + $data['addenda'] ?? null, + $data['namespaces'] ?? [], + $data['pdf_options'] ?? [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'type' => $this->type, + 'payment_form' => $this->payment_form, + 'payment_method' => $this->payment_method, + 'related_documents' => $this->related_documents !== null ? array_map(fn (\Facturapi\Models\RelatedDocumentInput $i): array => $i->toArray(), $this->related_documents) : null, + 'items' => $this->items !== null ? array_map(fn (\Facturapi\Models\LineItemEgresoInput $i): array => $i->toArray(), $this->items) : null, + 'use' => $this->use, + 'currency' => $this->currency, + 'exchange' => $this->exchange, + 'complements' => $this->complements !== null ? array_map(fn (\Facturapi\Models\CustomComplementInput $i): array => $i->toArray(), $this->complements) : null, + 'customer' => $this->customer, + 'status' => $this->status, + 'date' => $this->date, + 'address' => $this->address, + 'external_id' => $this->external_id, + 'idempotency_key' => $this->idempotency_key, + 'folio_number' => $this->folio_number, + 'series' => $this->series, + 'pdf_custom_section' => $this->pdf_custom_section, + 'addenda' => $this->addenda, + 'namespaces' => $this->namespaces, + 'pdf_options' => $this->pdf_options + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/InvoiceIngresoEditInput.php b/src/Models/InvoiceIngresoEditInput.php new file mode 100644 index 0000000..888d69d --- /dev/null +++ b/src/Models/InvoiceIngresoEditInput.php @@ -0,0 +1,102 @@ + is_array($i) ? \Facturapi\Models\LineItemInput::fromArray($i) : $i, $data['items']) : [], + $data['payment_form'] ?? null, + $data['payment_method'] ?? null, + $data['use'] ?? null, + $data['currency'] ?? null, + $data['exchange'] ?? 0, + $data['conditions'] ?? null, + isset($data['related_documents']) && is_array($data['related_documents']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\RelatedDocumentInput::fromArray($i) : $i, $data['related_documents']) : [], + $data['global'] ?? [], + $data['export'] ?? null, + isset($data['complements']) && is_array($data['complements']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\CustomComplementInput::fromArray($i) : $i, $data['complements']) : [], + $data['customer'] ?? null, + $data['status'] ?? null, + $data['date'] ?? null, + $data['address'] ?? null, + $data['external_id'] ?? null, + $data['idempotency_key'] ?? null, + $data['folio_number'] ?? 0, + $data['series'] ?? null, + $data['pdf_custom_section'] ?? null, + $data['addenda'] ?? null, + $data['namespaces'] ?? [], + $data['pdf_options'] ?? [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'type' => $this->type, + 'items' => $this->items !== null ? array_map(fn (\Facturapi\Models\LineItemInput $i): array => $i->toArray(), $this->items) : null, + 'payment_form' => $this->payment_form, + 'payment_method' => $this->payment_method, + 'use' => $this->use, + 'currency' => $this->currency, + 'exchange' => $this->exchange, + 'conditions' => $this->conditions, + 'related_documents' => $this->related_documents !== null ? array_map(fn (\Facturapi\Models\RelatedDocumentInput $i): array => $i->toArray(), $this->related_documents) : null, + 'global' => $this->global, + 'export' => $this->export, + 'complements' => $this->complements !== null ? array_map(fn (\Facturapi\Models\CustomComplementInput $i): array => $i->toArray(), $this->complements) : null, + 'customer' => $this->customer, + 'status' => $this->status, + 'date' => $this->date, + 'address' => $this->address, + 'external_id' => $this->external_id, + 'idempotency_key' => $this->idempotency_key, + 'folio_number' => $this->folio_number, + 'series' => $this->series, + 'pdf_custom_section' => $this->pdf_custom_section, + 'addenda' => $this->addenda, + 'namespaces' => $this->namespaces, + 'pdf_options' => $this->pdf_options + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/InvoiceIngresoInput.php b/src/Models/InvoiceIngresoInput.php new file mode 100644 index 0000000..c1bb7cc --- /dev/null +++ b/src/Models/InvoiceIngresoInput.php @@ -0,0 +1,102 @@ + is_array($i) ? \Facturapi\Models\LineItemInput::fromArray($i) : $i, $data['items']) : [], + $data['payment_form'] ?? '', + $data['payment_method'] ?? null, + $data['use'] ?? '', + $data['currency'] ?? null, + $data['exchange'] ?? 0, + $data['conditions'] ?? null, + isset($data['related_documents']) && is_array($data['related_documents']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\RelatedDocumentInput::fromArray($i) : $i, $data['related_documents']) : [], + $data['global'] ?? [], + $data['export'] ?? null, + isset($data['complements']) && is_array($data['complements']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\CartaPorteOrCustomComplementInput::fromArray($i) : $i, $data['complements']) : [], + $data['customer'] ?? '', + $data['status'] ?? null, + $data['date'] ?? null, + $data['address'] ?? null, + $data['external_id'] ?? null, + $data['idempotency_key'] ?? null, + $data['folio_number'] ?? 0, + $data['series'] ?? null, + $data['pdf_custom_section'] ?? null, + $data['addenda'] ?? null, + $data['namespaces'] ?? [], + $data['pdf_options'] ?? [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'type' => $this->type, + 'items' => $this->items !== null ? array_map(fn (\Facturapi\Models\LineItemInput $i): array => $i->toArray(), $this->items) : null, + 'payment_form' => $this->payment_form, + 'payment_method' => $this->payment_method, + 'use' => $this->use, + 'currency' => $this->currency, + 'exchange' => $this->exchange, + 'conditions' => $this->conditions, + 'related_documents' => $this->related_documents !== null ? array_map(fn (\Facturapi\Models\RelatedDocumentInput $i): array => $i->toArray(), $this->related_documents) : null, + 'global' => $this->global, + 'export' => $this->export, + 'complements' => $this->complements !== null ? array_map(fn (\Facturapi\Models\CartaPorteOrCustomComplementInput $i): array => $i->toArray(), $this->complements) : null, + 'customer' => $this->customer, + 'status' => $this->status, + 'date' => $this->date, + 'address' => $this->address, + 'external_id' => $this->external_id, + 'idempotency_key' => $this->idempotency_key, + 'folio_number' => $this->folio_number, + 'series' => $this->series, + 'pdf_custom_section' => $this->pdf_custom_section, + 'addenda' => $this->addenda, + 'namespaces' => $this->namespaces, + 'pdf_options' => $this->pdf_options + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/InvoiceNominaEditInput.php b/src/Models/InvoiceNominaEditInput.php new file mode 100644 index 0000000..beaff6d --- /dev/null +++ b/src/Models/InvoiceNominaEditInput.php @@ -0,0 +1,73 @@ + is_array($i) ? \Facturapi\Models\NominaOrCustomComplementInput::fromArray($i) : $i, $data['complements']) : [], + isset($data['related_documents']) && is_array($data['related_documents']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\RelatedDocumentInput::fromArray($i) : $i, $data['related_documents']) : [], + $data['customer'] ?? null, + $data['status'] ?? null, + $data['date'] ?? null, + $data['address'] ?? null, + $data['external_id'] ?? null, + $data['idempotency_key'] ?? null, + $data['folio_number'] ?? 0, + $data['series'] ?? null, + $data['pdf_custom_section'] ?? null, + $data['addenda'] ?? null, + $data['namespaces'] ?? [], + $data['pdf_options'] ?? [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'type' => $this->type, + 'complements' => $this->complements !== null ? array_map(fn (\Facturapi\Models\NominaOrCustomComplementInput $i): array => $i->toArray(), $this->complements) : null, + 'related_documents' => $this->related_documents !== null ? array_map(fn (\Facturapi\Models\RelatedDocumentInput $i): array => $i->toArray(), $this->related_documents) : null, + 'customer' => $this->customer, + 'status' => $this->status, + 'date' => $this->date, + 'address' => $this->address, + 'external_id' => $this->external_id, + 'idempotency_key' => $this->idempotency_key, + 'folio_number' => $this->folio_number, + 'series' => $this->series, + 'pdf_custom_section' => $this->pdf_custom_section, + 'addenda' => $this->addenda, + 'namespaces' => $this->namespaces, + 'pdf_options' => $this->pdf_options + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/InvoiceNominaInput.php b/src/Models/InvoiceNominaInput.php new file mode 100644 index 0000000..258b9d7 --- /dev/null +++ b/src/Models/InvoiceNominaInput.php @@ -0,0 +1,73 @@ + is_array($i) ? \Facturapi\Models\NominaOrCustomComplementInput::fromArray($i) : $i, $data['complements']) : [], + isset($data['related_documents']) && is_array($data['related_documents']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\RelatedDocumentInput::fromArray($i) : $i, $data['related_documents']) : [], + $data['customer'] ?? '', + $data['status'] ?? null, + $data['date'] ?? null, + $data['address'] ?? null, + $data['external_id'] ?? null, + $data['idempotency_key'] ?? null, + $data['folio_number'] ?? 0, + $data['series'] ?? null, + $data['pdf_custom_section'] ?? null, + $data['addenda'] ?? null, + $data['namespaces'] ?? [], + $data['pdf_options'] ?? [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'type' => $this->type, + 'complements' => $this->complements !== null ? array_map(fn (\Facturapi\Models\NominaOrCustomComplementInput $i): array => $i->toArray(), $this->complements) : null, + 'related_documents' => $this->related_documents !== null ? array_map(fn (\Facturapi\Models\RelatedDocumentInput $i): array => $i->toArray(), $this->related_documents) : null, + 'customer' => $this->customer, + 'status' => $this->status, + 'date' => $this->date, + 'address' => $this->address, + 'external_id' => $this->external_id, + 'idempotency_key' => $this->idempotency_key, + 'folio_number' => $this->folio_number, + 'series' => $this->series, + 'pdf_custom_section' => $this->pdf_custom_section, + 'addenda' => $this->addenda, + 'namespaces' => $this->namespaces, + 'pdf_options' => $this->pdf_options + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/InvoicePagoEditInput.php b/src/Models/InvoicePagoEditInput.php new file mode 100644 index 0000000..4b2690b --- /dev/null +++ b/src/Models/InvoicePagoEditInput.php @@ -0,0 +1,76 @@ + is_array($i) ? \Facturapi\Models\RelatedDocumentInput::fromArray($i) : $i, $data['related_documents']) : [], + $data['third_party'] ?? null, + isset($data['complements']) && is_array($data['complements']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\PagoOrCustomComplementInput::fromArray($i) : $i, $data['complements']) : [], + $data['customer'] ?? null, + $data['status'] ?? null, + $data['date'] ?? null, + $data['address'] ?? null, + $data['external_id'] ?? null, + $data['idempotency_key'] ?? null, + $data['folio_number'] ?? 0, + $data['series'] ?? null, + $data['pdf_custom_section'] ?? null, + $data['addenda'] ?? null, + $data['namespaces'] ?? [], + $data['pdf_options'] ?? [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'type' => $this->type, + 'related_documents' => $this->related_documents !== null ? array_map(fn (\Facturapi\Models\RelatedDocumentInput $i): array => $i->toArray(), $this->related_documents) : null, + 'third_party' => $this->third_party, + 'complements' => $this->complements !== null ? array_map(fn (\Facturapi\Models\PagoOrCustomComplementInput $i): array => $i->toArray(), $this->complements) : null, + 'customer' => $this->customer, + 'status' => $this->status, + 'date' => $this->date, + 'address' => $this->address, + 'external_id' => $this->external_id, + 'idempotency_key' => $this->idempotency_key, + 'folio_number' => $this->folio_number, + 'series' => $this->series, + 'pdf_custom_section' => $this->pdf_custom_section, + 'addenda' => $this->addenda, + 'namespaces' => $this->namespaces, + 'pdf_options' => $this->pdf_options + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/InvoicePagoInput.php b/src/Models/InvoicePagoInput.php new file mode 100644 index 0000000..3885875 --- /dev/null +++ b/src/Models/InvoicePagoInput.php @@ -0,0 +1,76 @@ + is_array($i) ? \Facturapi\Models\RelatedDocumentInput::fromArray($i) : $i, $data['related_documents']) : [], + $data['third_party'] ?? null, + isset($data['complements']) && is_array($data['complements']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\PagoOrCustomComplementInput::fromArray($i) : $i, $data['complements']) : [], + $data['customer'] ?? '', + $data['status'] ?? null, + $data['date'] ?? null, + $data['address'] ?? null, + $data['external_id'] ?? null, + $data['idempotency_key'] ?? null, + $data['folio_number'] ?? 0, + $data['series'] ?? null, + $data['pdf_custom_section'] ?? null, + $data['addenda'] ?? null, + $data['namespaces'] ?? [], + $data['pdf_options'] ?? [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'type' => $this->type, + 'related_documents' => $this->related_documents !== null ? array_map(fn (\Facturapi\Models\RelatedDocumentInput $i): array => $i->toArray(), $this->related_documents) : null, + 'third_party' => $this->third_party, + 'complements' => $this->complements !== null ? array_map(fn (\Facturapi\Models\PagoOrCustomComplementInput $i): array => $i->toArray(), $this->complements) : null, + 'customer' => $this->customer, + 'status' => $this->status, + 'date' => $this->date, + 'address' => $this->address, + 'external_id' => $this->external_id, + 'idempotency_key' => $this->idempotency_key, + 'folio_number' => $this->folio_number, + 'series' => $this->series, + 'pdf_custom_section' => $this->pdf_custom_section, + 'addenda' => $this->addenda, + 'namespaces' => $this->namespaces, + 'pdf_options' => $this->pdf_options + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/InvoiceProperties.php b/src/Models/InvoiceProperties.php new file mode 100644 index 0000000..fd9d8c2 --- /dev/null +++ b/src/Models/InvoiceProperties.php @@ -0,0 +1,128 @@ + is_array($i) ? \Facturapi\Models\LineItem::fromArray($i) : $i, $data['items']) : [], + isset($data['related_documents']) && is_array($data['related_documents']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\RelatedDocument::fromArray($i) : $i, $data['related_documents']) : [], + $data['received_payment_ids'] ?? [], + $data['target_invoice_ids'] ?? [], + $data['currency'] ?? null, + $data['exchange'] ?? 0, + isset($data['complements']) && is_array($data['complements']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\NominaOrCustomComplementProperties::fromArray($i) : $i, $data['complements']) : [], + $data['pdf_custom_section'] ?? null, + $data['addenda'] ?? null, + isset($data['namespaces']) && is_array($data['namespaces']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\NamespaceProperties::fromArray($i) : $i, $data['namespaces']) : [], + isset($data['stamp']) && is_array($data['stamp']) ? \Facturapi\Models\Stamp::fromArray($data['stamp']) : null + ); + } + + public function toArray(): array + { + return array_filter([ + 'status' => $this->status, + 'cancellation_status' => $this->cancellation_status, + 'canceled_at' => $this->canceled_at, + 'verification_url' => $this->verification_url, + 'date' => $this->date, + 'address' => $this->address, + 'type' => $this->type, + 'customer' => $this->customer?->toArray(), + 'total' => $this->total, + 'uuid' => $this->uuid, + 'folio_number' => $this->folio_number, + 'series' => $this->series, + 'external_id' => $this->external_id, + 'idempotency_key' => $this->idempotency_key, + 'payment_form' => $this->payment_form, + 'payment_method' => $this->payment_method, + 'use' => $this->use, + 'conditions' => $this->conditions, + 'total_payment_amount' => $this->total_payment_amount, + 'total_payment_amount_converted' => $this->total_payment_amount_converted, + 'is_ready_to_stamp' => $this->is_ready_to_stamp, + 'items' => $this->items !== null ? array_map(fn (\Facturapi\Models\LineItem $i): array => $i->toArray(), $this->items) : null, + 'related_documents' => $this->related_documents !== null ? array_map(fn (\Facturapi\Models\RelatedDocument $i): array => $i->toArray(), $this->related_documents) : null, + 'received_payment_ids' => $this->received_payment_ids, + 'target_invoice_ids' => $this->target_invoice_ids, + 'currency' => $this->currency, + 'exchange' => $this->exchange, + 'complements' => $this->complements !== null ? array_map(fn (\Facturapi\Models\NominaOrCustomComplementProperties $i): array => $i->toArray(), $this->complements) : null, + 'pdf_custom_section' => $this->pdf_custom_section, + 'addenda' => $this->addenda, + 'namespaces' => $this->namespaces !== null ? array_map(fn (\Facturapi\Models\NamespaceProperties $i): array => $i->toArray(), $this->namespaces) : null, + 'stamp' => $this->stamp?->toArray() + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/InvoiceReceiptInput.php b/src/Models/InvoiceReceiptInput.php new file mode 100644 index 0000000..e6ae72f --- /dev/null +++ b/src/Models/InvoiceReceiptInput.php @@ -0,0 +1,51 @@ + $this->customer, + 'use' => $this->use, + 'conditions' => $this->conditions, + 'folio_number' => $this->folio_number, + 'series' => $this->series, + 'pdf_custom_section' => $this->pdf_custom_section, + 'addenda' => $this->addenda, + 'namespaces' => $this->namespaces, + 'pdf_options' => $this->pdf_options + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/InvoiceSearchResult.php b/src/Models/InvoiceSearchResult.php new file mode 100644 index 0000000..5c80b11 --- /dev/null +++ b/src/Models/InvoiceSearchResult.php @@ -0,0 +1,47 @@ + is_array($i) ? \Facturapi\Models\Invoice::fromArray($i) : $i, $data['data']) : [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'page' => $this->page, + 'total_pages' => $this->total_pages, + 'total_results' => $this->total_results, + 'previous_cursor' => $this->previous_cursor, + 'next_cursor' => $this->next_cursor, + 'totals_are_capped' => $this->totals_are_capped, + 'data' => $this->data !== null ? array_map(fn (\Facturapi\Models\Invoice $i): array => $i->toArray(), $this->data) : null + ], fn (int|string|bool|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/InvoiceTrasladoEditInput.php b/src/Models/InvoiceTrasladoEditInput.php new file mode 100644 index 0000000..53592dc --- /dev/null +++ b/src/Models/InvoiceTrasladoEditInput.php @@ -0,0 +1,87 @@ + is_array($i) ? \Facturapi\Models\LineItemTrasladoInput::fromArray($i) : $i, $data['items']) : [], + isset($data['complements']) && is_array($data['complements']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\CustomComplementInput::fromArray($i) : $i, $data['complements']) : [], + $data['use'] ?? null, + $data['currency'] ?? null, + $data['exchange'] ?? 0, + isset($data['related_documents']) && is_array($data['related_documents']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\RelatedDocumentInput::fromArray($i) : $i, $data['related_documents']) : [], + $data['customer'] ?? null, + $data['status'] ?? null, + $data['date'] ?? null, + $data['address'] ?? null, + $data['external_id'] ?? null, + $data['idempotency_key'] ?? null, + $data['folio_number'] ?? 0, + $data['series'] ?? null, + $data['pdf_custom_section'] ?? null, + $data['addenda'] ?? null, + $data['namespaces'] ?? [], + $data['pdf_options'] ?? [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'type' => $this->type, + 'items' => $this->items !== null ? array_map(fn (\Facturapi\Models\LineItemTrasladoInput $i): array => $i->toArray(), $this->items) : null, + 'complements' => $this->complements !== null ? array_map(fn (\Facturapi\Models\CustomComplementInput $i): array => $i->toArray(), $this->complements) : null, + 'use' => $this->use, + 'currency' => $this->currency, + 'exchange' => $this->exchange, + 'related_documents' => $this->related_documents !== null ? array_map(fn (\Facturapi\Models\RelatedDocumentInput $i): array => $i->toArray(), $this->related_documents) : null, + 'customer' => $this->customer, + 'status' => $this->status, + 'date' => $this->date, + 'address' => $this->address, + 'external_id' => $this->external_id, + 'idempotency_key' => $this->idempotency_key, + 'folio_number' => $this->folio_number, + 'series' => $this->series, + 'pdf_custom_section' => $this->pdf_custom_section, + 'addenda' => $this->addenda, + 'namespaces' => $this->namespaces, + 'pdf_options' => $this->pdf_options + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/InvoiceTrasladoInput.php b/src/Models/InvoiceTrasladoInput.php new file mode 100644 index 0000000..c817890 --- /dev/null +++ b/src/Models/InvoiceTrasladoInput.php @@ -0,0 +1,87 @@ + is_array($i) ? \Facturapi\Models\LineItemTrasladoInput::fromArray($i) : $i, $data['items']) : [], + isset($data['complements']) && is_array($data['complements']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\CartaPorteOrCustomComplementInput::fromArray($i) : $i, $data['complements']) : [], + $data['use'] ?? null, + $data['currency'] ?? null, + $data['exchange'] ?? 0, + isset($data['related_documents']) && is_array($data['related_documents']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\RelatedDocumentInput::fromArray($i) : $i, $data['related_documents']) : [], + $data['customer'] ?? '', + $data['status'] ?? null, + $data['date'] ?? null, + $data['address'] ?? null, + $data['external_id'] ?? null, + $data['idempotency_key'] ?? null, + $data['folio_number'] ?? 0, + $data['series'] ?? null, + $data['pdf_custom_section'] ?? null, + $data['addenda'] ?? null, + $data['namespaces'] ?? [], + $data['pdf_options'] ?? [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'type' => $this->type, + 'items' => $this->items !== null ? array_map(fn (\Facturapi\Models\LineItemTrasladoInput $i): array => $i->toArray(), $this->items) : null, + 'complements' => $this->complements !== null ? array_map(fn (\Facturapi\Models\CartaPorteOrCustomComplementInput $i): array => $i->toArray(), $this->complements) : null, + 'use' => $this->use, + 'currency' => $this->currency, + 'exchange' => $this->exchange, + 'related_documents' => $this->related_documents !== null ? array_map(fn (\Facturapi\Models\RelatedDocumentInput $i): array => $i->toArray(), $this->related_documents) : null, + 'customer' => $this->customer, + 'status' => $this->status, + 'date' => $this->date, + 'address' => $this->address, + 'external_id' => $this->external_id, + 'idempotency_key' => $this->idempotency_key, + 'folio_number' => $this->folio_number, + 'series' => $this->series, + 'pdf_custom_section' => $this->pdf_custom_section, + 'addenda' => $this->addenda, + 'namespaces' => $this->namespaces, + 'pdf_options' => $this->pdf_options + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/InvoiceZipRequest.php b/src/Models/InvoiceZipRequest.php new file mode 100644 index 0000000..21622b1 --- /dev/null +++ b/src/Models/InvoiceZipRequest.php @@ -0,0 +1,69 @@ + is_string($i) ? (\Facturapi\Enums\InvoiceZipRequestInvoiceType::tryFrom($i) ?? $i) : $i, $data['invoice_types']) + : [], + $data['start_date'] ?? '', + $data['end_date'] ?? '', + isset($data['status']) && is_string($data['status']) ? \Facturapi\Enums\InvoiceZipRequestStatus::tryFrom($data['status']) : null, + $data['document_count'] ?? 0, + $data['processed_document'] ?? 0, + $data['failed_documents'] ?? [], + $data['scheduled_at'] ?? null + ); + } + + public function toArray(): array + { + return array_filter([ + 'id' => $this->id, + 'created_at' => $this->created_at, + 'livemode' => $this->livemode, + 'organization' => $this->organization, + 'issuer_type' => $this->issuer_type?->value, + 'invoice_types' => $this->invoice_types !== null + ? array_map(fn ($i) => $i instanceof \Facturapi\Enums\InvoiceZipRequestInvoiceType ? $i->value : $i, $this->invoice_types) + : null, + 'start_date' => $this->start_date, + 'end_date' => $this->end_date, + 'status' => $this->status?->value, + 'document_count' => $this->document_count, + 'processed_document' => $this->processed_document, + 'failed_documents' => $this->failed_documents, + 'scheduled_at' => $this->scheduled_at + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/InvoiceZipRequestCreateInput.php b/src/Models/InvoiceZipRequestCreateInput.php new file mode 100644 index 0000000..81c6bb4 --- /dev/null +++ b/src/Models/InvoiceZipRequestCreateInput.php @@ -0,0 +1,38 @@ + is_array($i) ? \Facturapi\Models\InvoiceZipRequestInvoiceType::fromArray($i) : $i, $data['invoice_types']) : [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'year' => $this->year, + 'month' => $this->month, + 'issuer_type' => $this->issuer_type, + 'invoice_types' => $this->invoice_types !== null ? array_map(fn (\Facturapi\Models\InvoiceZipRequestInvoiceType $i) => $i->toArray(), $this->invoice_types) : null + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/InvoiceZipRequestSearchResult.php b/src/Models/InvoiceZipRequestSearchResult.php new file mode 100644 index 0000000..afcb620 --- /dev/null +++ b/src/Models/InvoiceZipRequestSearchResult.php @@ -0,0 +1,47 @@ + is_array($i) ? \Facturapi\Models\InvoiceZipRequest::fromArray($i) : $i, $data['data']) : [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'page' => $this->page, + 'total_pages' => $this->total_pages, + 'total_results' => $this->total_results, + 'previous_cursor' => $this->previous_cursor, + 'next_cursor' => $this->next_cursor, + 'totals_are_capped' => $this->totals_are_capped, + 'data' => $this->data !== null ? array_map(fn (\Facturapi\Models\InvoiceZipRequest $i): array => $i->toArray(), $this->data) : null + ], fn (int|string|bool|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/InvoiceableCommonEditInput.php b/src/Models/InvoiceableCommonEditInput.php new file mode 100644 index 0000000..80cace9 --- /dev/null +++ b/src/Models/InvoiceableCommonEditInput.php @@ -0,0 +1,42 @@ + $this->folio_number, + 'series' => $this->series, + 'pdf_custom_section' => $this->pdf_custom_section, + 'addenda' => $this->addenda, + 'namespaces' => $this->namespaces, + 'pdf_options' => $this->pdf_options + ], fn (int|string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/InvoiceableCommonInput.php b/src/Models/InvoiceableCommonInput.php new file mode 100644 index 0000000..16b323a --- /dev/null +++ b/src/Models/InvoiceableCommonInput.php @@ -0,0 +1,42 @@ + $this->folio_number, + 'series' => $this->series, + 'pdf_custom_section' => $this->pdf_custom_section, + 'addenda' => $this->addenda, + 'namespaces' => $this->namespaces, + 'pdf_options' => $this->pdf_options + ], fn (int|string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/LeyendasFiscalesData.php b/src/Models/LeyendasFiscalesData.php new file mode 100644 index 0000000..e324542 --- /dev/null +++ b/src/Models/LeyendasFiscalesData.php @@ -0,0 +1,27 @@ + $this->leyendas + ], fn (array $v): true => $v !== null); + } +} diff --git a/src/Models/LeyendasFiscalesInput.php b/src/Models/LeyendasFiscalesInput.php new file mode 100644 index 0000000..9eeae09 --- /dev/null +++ b/src/Models/LeyendasFiscalesInput.php @@ -0,0 +1,30 @@ + $this->type, + 'data' => $this->data?->toArray() + ], fn (string|array $v): true => $v !== null); + } +} diff --git a/src/Models/LeyendasFiscalesProperties.php b/src/Models/LeyendasFiscalesProperties.php new file mode 100644 index 0000000..3d1b98c --- /dev/null +++ b/src/Models/LeyendasFiscalesProperties.php @@ -0,0 +1,30 @@ + $this->type, + 'data' => $this->data?->toArray() + ], fn (string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/LineItem.php b/src/Models/LineItem.php new file mode 100644 index 0000000..7c7bda9 --- /dev/null +++ b/src/Models/LineItem.php @@ -0,0 +1,48 @@ + $this->quantity, + 'discount' => $this->discount, + 'product' => $this->product?->toArray(), + 'parts' => $this->parts?->toArray(), + 'customs_keys' => $this->customs_keys, + 'complement' => $this->complement, + 'third_party' => $this->third_party, + 'property_tax_account' => $this->property_tax_account, + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/LineItemEgresoInput.php b/src/Models/LineItemEgresoInput.php new file mode 100644 index 0000000..662c461 --- /dev/null +++ b/src/Models/LineItemEgresoInput.php @@ -0,0 +1,47 @@ + is_array($i) ? \Facturapi\Models\PartInput::fromArray($i) : $i, $data['parts']) : [], + $data['customs_keys'] ?? [], + $data['complement'] ?? null, + $data['third_party'] ?? null + ); + } + + public function toArray(): array + { + return array_filter([ + 'quantity' => $this->quantity, + 'discount' => $this->discount, + 'product' => $this->product, + 'parts' => $this->parts !== null ? array_map(fn (\Facturapi\Models\PartInput $i): array => $i->toArray(), $this->parts) : null, + 'customs_keys' => $this->customs_keys, + 'complement' => $this->complement, + 'third_party' => $this->third_party + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/LineItemInput.php b/src/Models/LineItemInput.php new file mode 100644 index 0000000..f758091 --- /dev/null +++ b/src/Models/LineItemInput.php @@ -0,0 +1,50 @@ + is_array($i) ? \Facturapi\Models\PartInput::fromArray($i) : $i, $data['parts']) : [], + $data['customs_keys'] ?? [], + $data['complement'] ?? null, + $data['third_party'] ?? null, + $data['property_tax_account'] ?? [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'quantity' => $this->quantity, + 'discount' => $this->discount, + 'product' => $this->product, + 'parts' => $this->parts !== null ? array_map(fn (\Facturapi\Models\PartInput $i): array => $i->toArray(), $this->parts) : null, + 'customs_keys' => $this->customs_keys, + 'complement' => $this->complement, + 'third_party' => $this->third_party, + 'property_tax_account' => $this->property_tax_account + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/LineItemProduct.php b/src/Models/LineItemProduct.php new file mode 100644 index 0000000..374f77d --- /dev/null +++ b/src/Models/LineItemProduct.php @@ -0,0 +1,61 @@ + is_array($i) ? \Facturapi\Models\BaseTax::fromArray($i) : $i, $data['taxes']) : [], + isset($data['local_taxes']) && is_array($data['local_taxes']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\LocalTax::fromArray($i) : $i, $data['local_taxes']) : [], + $data['unit_key'] ?? '', + $data['unit_name'] ?? null, + $data['sku'] ?? null + ); + } + + public function toArray(): array + { + return array_filter([ + 'id' => $this->id, + 'description' => $this->description, + 'product_key' => $this->product_key, + 'price' => $this->price, + 'tax_included' => $this->tax_included, + 'taxability' => $this->taxability, + 'taxes' => $this->taxes !== null ? array_map(fn (\Facturapi\Models\BaseTax $i): array => $i->toArray(), $this->taxes) : null, + 'local_taxes' => $this->local_taxes !== null ? array_map(fn (\Facturapi\Models\LocalTax $i): array => $i->toArray(), $this->local_taxes) : null, + 'unit_key' => $this->unit_key, + 'unit_name' => $this->unit_name, + 'sku' => $this->sku + ], fn (float|bool|string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/LineItemProductEgresoInput.php b/src/Models/LineItemProductEgresoInput.php new file mode 100644 index 0000000..bf6ee1c --- /dev/null +++ b/src/Models/LineItemProductEgresoInput.php @@ -0,0 +1,55 @@ + is_array($i) ? \Facturapi\Models\BaseTax::fromArray($i) : $i, $data['taxes']) : [], + isset($data['local_taxes']) && is_array($data['local_taxes']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\LocalTax::fromArray($i) : $i, $data['local_taxes']) : [], + $data['unit_key'] ?? null, + $data['unit_name'] ?? null + ); + } + + public function toArray(): array + { + return array_filter([ + 'description' => $this->description, + 'product_key' => $this->product_key, + 'price' => $this->price, + 'tax_included' => $this->tax_included, + 'taxability' => $this->taxability, + 'taxes' => $this->taxes !== null ? array_map(fn (\Facturapi\Models\BaseTax $i): array => $i->toArray(), $this->taxes) : null, + 'local_taxes' => $this->local_taxes !== null ? array_map(fn (\Facturapi\Models\LocalTax $i): array => $i->toArray(), $this->local_taxes) : null, + 'unit_key' => $this->unit_key, + 'unit_name' => $this->unit_name + ], fn (float|bool|string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/LineItemProductInput.php b/src/Models/LineItemProductInput.php new file mode 100644 index 0000000..cc5b9e4 --- /dev/null +++ b/src/Models/LineItemProductInput.php @@ -0,0 +1,58 @@ + is_array($i) ? \Facturapi\Models\BaseTax::fromArray($i) : $i, $data['taxes']) : [], + isset($data['local_taxes']) && is_array($data['local_taxes']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\LocalTax::fromArray($i) : $i, $data['local_taxes']) : [], + $data['unit_key'] ?? '', + $data['unit_name'] ?? null, + $data['sku'] ?? null + ); + } + + public function toArray(): array + { + return array_filter([ + 'description' => $this->description, + 'product_key' => $this->product_key, + 'price' => $this->price, + 'tax_included' => $this->tax_included, + 'taxability' => $this->taxability, + 'taxes' => $this->taxes !== null ? array_map(fn (\Facturapi\Models\BaseTax $i): array => $i->toArray(), $this->taxes) : null, + 'local_taxes' => $this->local_taxes !== null ? array_map(fn (\Facturapi\Models\LocalTax $i): array => $i->toArray(), $this->local_taxes) : null, + 'unit_key' => $this->unit_key, + 'unit_name' => $this->unit_name, + 'sku' => $this->sku + ], fn (float|bool|string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/LineItemTrasladoInput.php b/src/Models/LineItemTrasladoInput.php new file mode 100644 index 0000000..18ecf5c --- /dev/null +++ b/src/Models/LineItemTrasladoInput.php @@ -0,0 +1,44 @@ + is_array($i) ? \Facturapi\Models\PartInput::fromArray($i) : $i, $data['parts']) : [], + $data['third_party'] ?? [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'quantity' => $this->quantity, + 'product' => $this->product, + 'customs_keys' => $this->customs_keys, + 'complement' => $this->complement, + 'parts' => $this->parts !== null ? array_map(fn (\Facturapi\Models\PartInput $i): array => $i->toArray(), $this->parts) : null, + 'third_party' => $this->third_party + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/LineItemTrasladoProductInput.php b/src/Models/LineItemTrasladoProductInput.php new file mode 100644 index 0000000..d846ee9 --- /dev/null +++ b/src/Models/LineItemTrasladoProductInput.php @@ -0,0 +1,39 @@ + $this->description, + 'product_key' => $this->product_key, + 'unit_key' => $this->unit_key, + 'unit_name' => $this->unit_name, + 'sku' => $this->sku + ], fn (?string $v): bool => $v !== null); + } +} diff --git a/src/Models/LocalTax.php b/src/Models/LocalTax.php new file mode 100644 index 0000000..bda0824 --- /dev/null +++ b/src/Models/LocalTax.php @@ -0,0 +1,36 @@ + $this->rate, + 'base' => $this->base, + 'type' => $this->type, + 'withholding' => $this->withholding, + ], fn (float|string|bool|null $v): bool => $v !== null); + } +} diff --git a/src/Models/NamespaceProperties.php b/src/Models/NamespaceProperties.php new file mode 100644 index 0000000..b725d45 --- /dev/null +++ b/src/Models/NamespaceProperties.php @@ -0,0 +1,33 @@ + $this->prefix, + 'uri' => $this->uri, + 'schema_location' => $this->schema_location + ], fn (?string $v): bool => $v !== null); + } +} diff --git a/src/Models/NominaAccionesInput.php b/src/Models/NominaAccionesInput.php new file mode 100644 index 0000000..f3f75d4 --- /dev/null +++ b/src/Models/NominaAccionesInput.php @@ -0,0 +1,30 @@ + $this->valor_mercado, + 'precio_al_otorgarse' => $this->precio_al_otorgarse + ], fn (float $v): true => $v !== null); + } +} diff --git a/src/Models/NominaAccionesProperties.php b/src/Models/NominaAccionesProperties.php new file mode 100644 index 0000000..8c3a859 --- /dev/null +++ b/src/Models/NominaAccionesProperties.php @@ -0,0 +1,30 @@ + $this->valor_mercado, + 'precio_al_otorgarse' => $this->precio_al_otorgarse + ], fn (?float $v): bool => $v !== null); + } +} diff --git a/src/Models/NominaCompensacionInput.php b/src/Models/NominaCompensacionInput.php new file mode 100644 index 0000000..bed00da --- /dev/null +++ b/src/Models/NominaCompensacionInput.php @@ -0,0 +1,33 @@ + $this->saldo_a_favor, + 'ano' => $this->ano, + 'remanente_sal_fav' => $this->remanente_sal_fav + ], fn (int|float $v): true => $v !== null); + } +} diff --git a/src/Models/NominaCompensacionProperties.php b/src/Models/NominaCompensacionProperties.php new file mode 100644 index 0000000..28155ea --- /dev/null +++ b/src/Models/NominaCompensacionProperties.php @@ -0,0 +1,33 @@ + $this->saldo_a_favor, + 'ano' => $this->ano, + 'remanente_sal_fav' => $this->remanente_sal_fav + ], fn (int|float|null $v): bool => $v !== null); + } +} diff --git a/src/Models/NominaComplementDataDirectProperties.php b/src/Models/NominaComplementDataDirectProperties.php new file mode 100644 index 0000000..7112347 --- /dev/null +++ b/src/Models/NominaComplementDataDirectProperties.php @@ -0,0 +1,39 @@ + $this->tipo_nomina, + 'fecha_pago' => $this->fecha_pago, + 'fecha_inicial_pago' => $this->fecha_inicial_pago, + 'fecha_final_pago' => $this->fecha_final_pago, + 'num_dias_pagados' => $this->num_dias_pagados + ], fn (string|float|null $v): bool => $v !== null); + } +} diff --git a/src/Models/NominaComplementDataInput.php b/src/Models/NominaComplementDataInput.php new file mode 100644 index 0000000..369813c --- /dev/null +++ b/src/Models/NominaComplementDataInput.php @@ -0,0 +1,61 @@ + is_array($i) ? \Facturapi\Models\NominaDeduccionInput::fromArray($i) : $i, $data['deducciones']) : [], + $data['otros_pagos'] ?? [], + isset($data['incapacidades']) && is_array($data['incapacidades']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\NominaIncapacidadInput::fromArray($i) : $i, $data['incapacidades']) : [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'tipo_nomina' => $this->tipo_nomina, + 'fecha_pago' => $this->fecha_pago, + 'fecha_inicial_pago' => $this->fecha_inicial_pago, + 'fecha_final_pago' => $this->fecha_final_pago, + 'num_dias_pagados' => $this->num_dias_pagados, + 'emisor' => $this->emisor?->toArray(), + 'receptor' => $this->receptor?->toArray(), + 'percepciones' => $this->percepciones?->toArray(), + 'deducciones' => $this->deducciones !== null ? array_map(fn (\Facturapi\Models\NominaDeduccionInput $i): array => $i->toArray(), $this->deducciones) : null, + 'otros_pagos' => $this->otros_pagos, + 'incapacidades' => $this->incapacidades !== null ? array_map(fn (\Facturapi\Models\NominaIncapacidadInput $i): array => $i->toArray(), $this->incapacidades) : null + ], fn (string|float|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/NominaComplementDataNestedInput.php b/src/Models/NominaComplementDataNestedInput.php new file mode 100644 index 0000000..46e624a --- /dev/null +++ b/src/Models/NominaComplementDataNestedInput.php @@ -0,0 +1,46 @@ + is_array($i) ? \Facturapi\Models\NominaDeduccionInput::fromArray($i) : $i, $data['deducciones']) : [], + $data['otros_pagos'] ?? [], + isset($data['incapacidades']) && is_array($data['incapacidades']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\NominaIncapacidadInput::fromArray($i) : $i, $data['incapacidades']) : [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'emisor' => $this->emisor?->toArray(), + 'receptor' => $this->receptor?->toArray(), + 'percepciones' => $this->percepciones?->toArray(), + 'deducciones' => $this->deducciones !== null ? array_map(fn (\Facturapi\Models\NominaDeduccionInput $i): array => $i->toArray(), $this->deducciones) : null, + 'otros_pagos' => $this->otros_pagos, + 'incapacidades' => $this->incapacidades !== null ? array_map(fn (\Facturapi\Models\NominaIncapacidadInput $i): array => $i->toArray(), $this->incapacidades) : null + ], fn (?array $v): bool => $v !== null); + } +} diff --git a/src/Models/NominaComplementDataNestedProperties.php b/src/Models/NominaComplementDataNestedProperties.php new file mode 100644 index 0000000..8b92564 --- /dev/null +++ b/src/Models/NominaComplementDataNestedProperties.php @@ -0,0 +1,46 @@ + is_array($i) ? \Facturapi\Models\NominaDeduccionProperties::fromArray($i) : $i, $data['deducciones']) : [], + $data['otros_pagos'] ?? [], + isset($data['incapacidades']) && is_array($data['incapacidades']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\NominaIncapacidadProperties::fromArray($i) : $i, $data['incapacidades']) : [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'emisor' => $this->emisor?->toArray(), + 'receptor' => $this->receptor?->toArray(), + 'percepciones' => $this->percepciones?->toArray(), + 'deducciones' => $this->deducciones !== null ? array_map(fn (\Facturapi\Models\NominaDeduccionProperties $i): array => $i->toArray(), $this->deducciones) : null, + 'otros_pagos' => $this->otros_pagos, + 'incapacidades' => $this->incapacidades !== null ? array_map(fn (\Facturapi\Models\NominaIncapacidadProperties $i): array => $i->toArray(), $this->incapacidades) : null + ], fn (?array $v): bool => $v !== null); + } +} diff --git a/src/Models/NominaComplementDataProperties.php b/src/Models/NominaComplementDataProperties.php new file mode 100644 index 0000000..6112cb2 --- /dev/null +++ b/src/Models/NominaComplementDataProperties.php @@ -0,0 +1,61 @@ + is_array($i) ? \Facturapi\Models\NominaDeduccionProperties::fromArray($i) : $i, $data['deducciones']) : [], + $data['otros_pagos'] ?? [], + isset($data['incapacidades']) && is_array($data['incapacidades']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\NominaIncapacidadProperties::fromArray($i) : $i, $data['incapacidades']) : [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'tipo_nomina' => $this->tipo_nomina, + 'fecha_pago' => $this->fecha_pago, + 'fecha_inicial_pago' => $this->fecha_inicial_pago, + 'fecha_final_pago' => $this->fecha_final_pago, + 'num_dias_pagados' => $this->num_dias_pagados, + 'emisor' => $this->emisor?->toArray(), + 'receptor' => $this->receptor?->toArray(), + 'percepciones' => $this->percepciones?->toArray(), + 'deducciones' => $this->deducciones !== null ? array_map(fn (\Facturapi\Models\NominaDeduccionProperties $i): array => $i->toArray(), $this->deducciones) : null, + 'otros_pagos' => $this->otros_pagos, + 'incapacidades' => $this->incapacidades !== null ? array_map(fn (\Facturapi\Models\NominaIncapacidadProperties $i): array => $i->toArray(), $this->incapacidades) : null + ], fn (string|float|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/NominaComplementInput.php b/src/Models/NominaComplementInput.php new file mode 100644 index 0000000..37e1b9c --- /dev/null +++ b/src/Models/NominaComplementInput.php @@ -0,0 +1,30 @@ + $this->type, + 'data' => $this->data?->toArray() + ], fn (string|array $v): true => $v !== null); + } +} diff --git a/src/Models/NominaComplementProperties.php b/src/Models/NominaComplementProperties.php new file mode 100644 index 0000000..e88dd50 --- /dev/null +++ b/src/Models/NominaComplementProperties.php @@ -0,0 +1,30 @@ + $this->type, + 'data' => $this->data?->toArray() + ], fn (string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/NominaDeduccionInput.php b/src/Models/NominaDeduccionInput.php new file mode 100644 index 0000000..2a05a15 --- /dev/null +++ b/src/Models/NominaDeduccionInput.php @@ -0,0 +1,36 @@ + $this->tipo_deduccion, + 'concepto' => $this->concepto, + 'clave' => $this->clave, + 'importe' => $this->importe + ], fn (string|float|null $v): bool => $v !== null); + } +} diff --git a/src/Models/NominaDeduccionProperties.php b/src/Models/NominaDeduccionProperties.php new file mode 100644 index 0000000..9bd9a22 --- /dev/null +++ b/src/Models/NominaDeduccionProperties.php @@ -0,0 +1,36 @@ + $this->tipo_deduccion, + 'concepto' => $this->concepto, + 'clave' => $this->clave, + 'importe' => $this->importe + ], fn (string|float|null $v): bool => $v !== null); + } +} diff --git a/src/Models/NominaEmisorProperties.php b/src/Models/NominaEmisorProperties.php new file mode 100644 index 0000000..7a242cf --- /dev/null +++ b/src/Models/NominaEmisorProperties.php @@ -0,0 +1,36 @@ + $this->curp, + 'registro_patronal' => $this->registro_patronal, + 'rfc_patron_origen' => $this->rfc_patron_origen, + 'entidad_sncf' => $this->entidad_sncf + ], fn (string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/NominaHorasExtraInput.php b/src/Models/NominaHorasExtraInput.php new file mode 100644 index 0000000..e802914 --- /dev/null +++ b/src/Models/NominaHorasExtraInput.php @@ -0,0 +1,36 @@ + $this->dias, + 'tipo_horas' => $this->tipo_horas, + 'horas_extra' => $this->horas_extra, + 'importe_pagado' => $this->importe_pagado + ], fn (string|int|float $v): true => $v !== null); + } +} diff --git a/src/Models/NominaHorasExtraProperties.php b/src/Models/NominaHorasExtraProperties.php new file mode 100644 index 0000000..e1aa13f --- /dev/null +++ b/src/Models/NominaHorasExtraProperties.php @@ -0,0 +1,36 @@ + $this->dias, + 'tipo_horas' => $this->tipo_horas, + 'horas_extra' => $this->horas_extra, + 'importe_pagado' => $this->importe_pagado + ], fn (string|int|float|null $v): bool => $v !== null); + } +} diff --git a/src/Models/NominaIncapacidadInput.php b/src/Models/NominaIncapacidadInput.php new file mode 100644 index 0000000..4dabaea --- /dev/null +++ b/src/Models/NominaIncapacidadInput.php @@ -0,0 +1,33 @@ + $this->dias_incapacidad, + 'tipo_incapacidad' => $this->tipo_incapacidad, + 'importe_monetario' => $this->importe_monetario + ], fn (int|string|float|null $v): bool => $v !== null); + } +} diff --git a/src/Models/NominaIncapacidadProperties.php b/src/Models/NominaIncapacidadProperties.php new file mode 100644 index 0000000..474ad49 --- /dev/null +++ b/src/Models/NominaIncapacidadProperties.php @@ -0,0 +1,33 @@ + $this->dias_incapacidad, + 'tipo_incapacidad' => $this->tipo_incapacidad, + 'importe_monetario' => $this->importe_monetario + ], fn (int|string|float|null $v): bool => $v !== null); + } +} diff --git a/src/Models/NominaJubilacionInput.php b/src/Models/NominaJubilacionInput.php new file mode 100644 index 0000000..a5d8bb9 --- /dev/null +++ b/src/Models/NominaJubilacionInput.php @@ -0,0 +1,39 @@ + $this->total_una_exhibicion, + 'total_parcialidad' => $this->total_parcialidad, + 'monto_diario' => $this->monto_diario, + 'ingreso_acumulable' => $this->ingreso_acumulable, + 'ingreso_no_acumulable' => $this->ingreso_no_acumulable + ], fn (?float $v): bool => $v !== null); + } +} diff --git a/src/Models/NominaJubilacionProperties.php b/src/Models/NominaJubilacionProperties.php new file mode 100644 index 0000000..ed767be --- /dev/null +++ b/src/Models/NominaJubilacionProperties.php @@ -0,0 +1,39 @@ + $this->total_una_exhibicion, + 'total_parcialidad' => $this->total_parcialidad, + 'monto_diario' => $this->monto_diario, + 'ingreso_acumulable' => $this->ingreso_acumulable, + 'ingreso_no_acumulable' => $this->ingreso_no_acumulable + ], fn (?float $v): bool => $v !== null); + } +} diff --git a/src/Models/NominaOrCustomComplementInput.php b/src/Models/NominaOrCustomComplementInput.php new file mode 100644 index 0000000..5eae8fd --- /dev/null +++ b/src/Models/NominaOrCustomComplementInput.php @@ -0,0 +1,27 @@ + $this->type + ], fn (string $v): true => $v !== null); + } +} diff --git a/src/Models/NominaOrCustomComplementProperties.php b/src/Models/NominaOrCustomComplementProperties.php new file mode 100644 index 0000000..961b652 --- /dev/null +++ b/src/Models/NominaOrCustomComplementProperties.php @@ -0,0 +1,27 @@ + $this->type + ], fn (?string $v): bool => $v !== null); + } +} diff --git a/src/Models/NominaOtroPagoDirectProperties.php b/src/Models/NominaOtroPagoDirectProperties.php new file mode 100644 index 0000000..a5aeed6 --- /dev/null +++ b/src/Models/NominaOtroPagoDirectProperties.php @@ -0,0 +1,39 @@ + $this->tipo_otro_pago, + 'clave' => $this->clave, + 'concepto' => $this->concepto, + 'importe' => $this->importe, + 'subsidio_causado' => $this->subsidio_causado + ], fn (string|float|null $v): bool => $v !== null); + } +} diff --git a/src/Models/NominaOtroPagoInput.php b/src/Models/NominaOtroPagoInput.php new file mode 100644 index 0000000..91463fc --- /dev/null +++ b/src/Models/NominaOtroPagoInput.php @@ -0,0 +1,42 @@ + $this->tipo_otro_pago, + 'clave' => $this->clave, + 'concepto' => $this->concepto, + 'importe' => $this->importe, + 'subsidio_causado' => $this->subsidio_causado, + 'compensacion_saldos_a_favor' => $this->compensacion_saldos_a_favor?->toArray() + ], fn (string|float|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/NominaPercepcionDirectProperties.php b/src/Models/NominaPercepcionDirectProperties.php new file mode 100644 index 0000000..8696ff9 --- /dev/null +++ b/src/Models/NominaPercepcionDirectProperties.php @@ -0,0 +1,39 @@ + $this->tipo_percepcion, + 'concepto' => $this->concepto, + 'clave' => $this->clave, + 'importe_gravado' => $this->importe_gravado, + 'importe_exento' => $this->importe_exento + ], fn (string|float|null $v): bool => $v !== null); + } +} diff --git a/src/Models/NominaPercepcionInput.php b/src/Models/NominaPercepcionInput.php new file mode 100644 index 0000000..8c2b905 --- /dev/null +++ b/src/Models/NominaPercepcionInput.php @@ -0,0 +1,47 @@ + is_array($i) ? \Facturapi\Models\NominaHorasExtraInput::fromArray($i) : $i, $data['horas_extra']) : [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'tipo_percepcion' => $this->tipo_percepcion, + 'concepto' => $this->concepto, + 'clave' => $this->clave, + 'importe_gravado' => $this->importe_gravado, + 'importe_exento' => $this->importe_exento, + 'acciones_o_titulos' => $this->acciones_o_titulos?->toArray(), + 'horas_extra' => $this->horas_extra !== null ? array_map(fn (\Facturapi\Models\NominaHorasExtraInput $i): array => $i->toArray(), $this->horas_extra) : null + ], fn (string|float|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/NominaPercepcionNestedInput.php b/src/Models/NominaPercepcionNestedInput.php new file mode 100644 index 0000000..504d3e4 --- /dev/null +++ b/src/Models/NominaPercepcionNestedInput.php @@ -0,0 +1,32 @@ + is_array($i) ? \Facturapi\Models\NominaHorasExtraInput::fromArray($i) : $i, $data['horas_extra']) : [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'acciones_o_titulos' => $this->acciones_o_titulos?->toArray(), + 'horas_extra' => $this->horas_extra !== null ? array_map(fn (\Facturapi\Models\NominaHorasExtraInput $i): array => $i->toArray(), $this->horas_extra) : null + ], fn (?array $v): bool => $v !== null); + } +} diff --git a/src/Models/NominaPercepcionNestedProperties.php b/src/Models/NominaPercepcionNestedProperties.php new file mode 100644 index 0000000..e630948 --- /dev/null +++ b/src/Models/NominaPercepcionNestedProperties.php @@ -0,0 +1,32 @@ + is_array($i) ? \Facturapi\Models\NominaHorasExtraProperties::fromArray($i) : $i, $data['horas_extra']) : [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'acciones_o_titulos' => $this->acciones_o_titulos?->toArray(), + 'horas_extra' => $this->horas_extra !== null ? array_map(fn (\Facturapi\Models\NominaHorasExtraProperties $i): array => $i->toArray(), $this->horas_extra) : null + ], fn (?array $v): bool => $v !== null); + } +} diff --git a/src/Models/NominaPercepcionProperties.php b/src/Models/NominaPercepcionProperties.php new file mode 100644 index 0000000..e256e4f --- /dev/null +++ b/src/Models/NominaPercepcionProperties.php @@ -0,0 +1,47 @@ + is_array($i) ? \Facturapi\Models\NominaHorasExtraProperties::fromArray($i) : $i, $data['horas_extra']) : [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'tipo_percepcion' => $this->tipo_percepcion, + 'concepto' => $this->concepto, + 'clave' => $this->clave, + 'importe_gravado' => $this->importe_gravado, + 'importe_exento' => $this->importe_exento, + 'acciones_o_titulos' => $this->acciones_o_titulos?->toArray(), + 'horas_extra' => $this->horas_extra !== null ? array_map(fn (\Facturapi\Models\NominaHorasExtraProperties $i): array => $i->toArray(), $this->horas_extra) : null + ], fn (string|float|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/NominaPercepcionesInput.php b/src/Models/NominaPercepcionesInput.php new file mode 100644 index 0000000..a7b340f --- /dev/null +++ b/src/Models/NominaPercepcionesInput.php @@ -0,0 +1,35 @@ + is_array($i) ? \Facturapi\Models\NominaPercepcionInput::fromArray($i) : $i, $data['percepcion']) : [], + isset($data['jubilacion_pension_retiro']) && is_array($data['jubilacion_pension_retiro']) ? \Facturapi\Models\NominaJubilacionInput::fromArray($data['jubilacion_pension_retiro']) : null, + isset($data['separacion_indemnizacion']) && is_array($data['separacion_indemnizacion']) ? \Facturapi\Models\NominaSeparacionInput::fromArray($data['separacion_indemnizacion']) : null + ); + } + + public function toArray(): array + { + return array_filter([ + 'percepcion' => $this->percepcion !== null ? array_map(fn (\Facturapi\Models\NominaPercepcionInput $i): array => $i->toArray(), $this->percepcion) : null, + 'jubilacion_pension_retiro' => $this->jubilacion_pension_retiro?->toArray(), + 'separacion_indemnizacion' => $this->separacion_indemnizacion?->toArray() + ], fn (?array $v): bool => $v !== null); + } +} diff --git a/src/Models/NominaPercepcionesProperties.php b/src/Models/NominaPercepcionesProperties.php new file mode 100644 index 0000000..6b5904c --- /dev/null +++ b/src/Models/NominaPercepcionesProperties.php @@ -0,0 +1,35 @@ + is_array($i) ? \Facturapi\Models\NominaPercepcionProperties::fromArray($i) : $i, $data['percepcion']) : [], + isset($data['jubilacion_pension_retiro']) && is_array($data['jubilacion_pension_retiro']) ? \Facturapi\Models\NominaJubilacionProperties::fromArray($data['jubilacion_pension_retiro']) : null, + isset($data['separacion_indemnizacion']) && is_array($data['separacion_indemnizacion']) ? \Facturapi\Models\NominaSeparacionProperties::fromArray($data['separacion_indemnizacion']) : null + ); + } + + public function toArray(): array + { + return array_filter([ + 'percepcion' => $this->percepcion !== null ? array_map(fn (\Facturapi\Models\NominaPercepcionProperties $i): array => $i->toArray(), $this->percepcion) : null, + 'jubilacion_pension_retiro' => $this->jubilacion_pension_retiro?->toArray(), + 'separacion_indemnizacion' => $this->separacion_indemnizacion?->toArray() + ], fn (?array $v): bool => $v !== null); + } +} diff --git a/src/Models/NominaReceptorDirectProperties.php b/src/Models/NominaReceptorDirectProperties.php new file mode 100644 index 0000000..a7af0d1 --- /dev/null +++ b/src/Models/NominaReceptorDirectProperties.php @@ -0,0 +1,78 @@ + $this->curp, + 'num_seguridad_social' => $this->num_seguridad_social, + 'fecha_inicio_rel_laboral' => $this->fecha_inicio_rel_laboral, + 'antiguedad' => $this->antiguedad, + 'tipo_contrato' => $this->tipo_contrato, + 'sindicalizado' => $this->sindicalizado, + 'tipo_jornada' => $this->tipo_jornada, + 'tipo_regimen' => $this->tipo_regimen, + 'num_empleado' => $this->num_empleado, + 'departamento' => $this->departamento, + 'puesto' => $this->puesto, + 'riesgo_puesto' => $this->riesgo_puesto, + 'periodicidad_pago' => $this->periodicidad_pago, + 'banco' => $this->banco, + 'cuenta_bancaria' => $this->cuenta_bancaria, + 'salario_base_cot_apor' => $this->salario_base_cot_apor, + 'salario_diario_integrado' => $this->salario_diario_integrado, + 'clave_ent_fed' => $this->clave_ent_fed + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/NominaReceptorInput.php b/src/Models/NominaReceptorInput.php new file mode 100644 index 0000000..be6d332 --- /dev/null +++ b/src/Models/NominaReceptorInput.php @@ -0,0 +1,81 @@ + $this->curp, + 'num_seguridad_social' => $this->num_seguridad_social, + 'fecha_inicio_rel_laboral' => $this->fecha_inicio_rel_laboral, + 'antiguedad' => $this->antiguedad, + 'tipo_contrato' => $this->tipo_contrato, + 'sindicalizado' => $this->sindicalizado, + 'tipo_jornada' => $this->tipo_jornada, + 'tipo_regimen' => $this->tipo_regimen, + 'num_empleado' => $this->num_empleado, + 'departamento' => $this->departamento, + 'puesto' => $this->puesto, + 'riesgo_puesto' => $this->riesgo_puesto, + 'periodicidad_pago' => $this->periodicidad_pago, + 'banco' => $this->banco, + 'cuenta_bancaria' => $this->cuenta_bancaria, + 'salario_base_cot_apor' => $this->salario_base_cot_apor, + 'salario_diario_integrado' => $this->salario_diario_integrado, + 'clave_ent_fed' => $this->clave_ent_fed, + 'sub_contratacion' => $this->sub_contratacion + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/NominaReceptorNestedInput.php b/src/Models/NominaReceptorNestedInput.php new file mode 100644 index 0000000..e2ee7d6 --- /dev/null +++ b/src/Models/NominaReceptorNestedInput.php @@ -0,0 +1,27 @@ + $this->sub_contratacion + ], fn (?array $v): bool => $v !== null); + } +} diff --git a/src/Models/NominaReceptorNestedProperties.php b/src/Models/NominaReceptorNestedProperties.php new file mode 100644 index 0000000..e299bc7 --- /dev/null +++ b/src/Models/NominaReceptorNestedProperties.php @@ -0,0 +1,29 @@ + is_array($i) ? \Facturapi\Models\NominaSubContratacionProperties::fromArray($i) : $i, $data['sub_contratacion']) : [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'sub_contratacion' => $this->sub_contratacion !== null ? array_map(fn (\Facturapi\Models\NominaSubContratacionProperties $i): array => $i->toArray(), $this->sub_contratacion) : null + ], fn (?array $v): bool => $v !== null); + } +} diff --git a/src/Models/NominaReceptorProperties.php b/src/Models/NominaReceptorProperties.php new file mode 100644 index 0000000..fc2a4ef --- /dev/null +++ b/src/Models/NominaReceptorProperties.php @@ -0,0 +1,83 @@ + is_array($i) ? \Facturapi\Models\NominaSubContratacionProperties::fromArray($i) : $i, $data['sub_contratacion']) : [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'curp' => $this->curp, + 'num_seguridad_social' => $this->num_seguridad_social, + 'fecha_inicio_rel_laboral' => $this->fecha_inicio_rel_laboral, + 'antiguedad' => $this->antiguedad, + 'tipo_contrato' => $this->tipo_contrato, + 'sindicalizado' => $this->sindicalizado, + 'tipo_jornada' => $this->tipo_jornada, + 'tipo_regimen' => $this->tipo_regimen, + 'num_empleado' => $this->num_empleado, + 'departamento' => $this->departamento, + 'puesto' => $this->puesto, + 'riesgo_puesto' => $this->riesgo_puesto, + 'periodicidad_pago' => $this->periodicidad_pago, + 'banco' => $this->banco, + 'cuenta_bancaria' => $this->cuenta_bancaria, + 'salario_base_cot_apor' => $this->salario_base_cot_apor, + 'salario_diario_integrado' => $this->salario_diario_integrado, + 'clave_ent_fed' => $this->clave_ent_fed, + 'sub_contratacion' => $this->sub_contratacion !== null ? array_map(fn (\Facturapi\Models\NominaSubContratacionProperties $i): array => $i->toArray(), $this->sub_contratacion) : null + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/NominaSeparacionInput.php b/src/Models/NominaSeparacionInput.php new file mode 100644 index 0000000..68a5994 --- /dev/null +++ b/src/Models/NominaSeparacionInput.php @@ -0,0 +1,39 @@ + $this->total_pagado, + 'num_anos_servicio' => $this->num_anos_servicio, + 'ultimo_sueldo_mens_ord' => $this->ultimo_sueldo_mens_ord, + 'ingreso_acumulable' => $this->ingreso_acumulable, + 'ingreso_no_acumulable' => $this->ingreso_no_acumulable + ], fn (int|float $v): true => $v !== null); + } +} diff --git a/src/Models/NominaSeparacionProperties.php b/src/Models/NominaSeparacionProperties.php new file mode 100644 index 0000000..48d1593 --- /dev/null +++ b/src/Models/NominaSeparacionProperties.php @@ -0,0 +1,39 @@ + $this->total_pagado, + 'num_anos_servicio' => $this->num_anos_servicio, + 'ultimo_sueldo_mens_ord' => $this->ultimo_sueldo_mens_ord, + 'ingreso_acumulable' => $this->ingreso_acumulable, + 'ingreso_no_acumulable' => $this->ingreso_no_acumulable + ], fn (int|float|null $v): bool => $v !== null); + } +} diff --git a/src/Models/NominaSubContratacionProperties.php b/src/Models/NominaSubContratacionProperties.php new file mode 100644 index 0000000..cb5696f --- /dev/null +++ b/src/Models/NominaSubContratacionProperties.php @@ -0,0 +1,30 @@ + $this->rfc_labora, + 'porcentaje_tiempo' => $this->porcentaje_tiempo + ], fn (string|float|null $v): bool => $v !== null); + } +} diff --git a/src/Models/OkResponse.php b/src/Models/OkResponse.php new file mode 100644 index 0000000..ced5e4d --- /dev/null +++ b/src/Models/OkResponse.php @@ -0,0 +1,34 @@ +ok = $ok; + $this->success = $success ?? $ok; + } + + public static function fromArray(array $data): self + { + $val = (bool) ($data['ok'] ?? $data['success'] ?? false); + + return new self($val, $val); + } + + public function toArray(): array + { + return [ + 'ok' => $this->ok, + 'success' => $this->success, + ]; + } +} diff --git a/src/Models/Organization.php b/src/Models/Organization.php new file mode 100644 index 0000000..64c7737 --- /dev/null +++ b/src/Models/Organization.php @@ -0,0 +1,63 @@ + $this->id, + 'name' => $this->name, + 'logo_url' => $this->logo_url, + 'timezone' => $this->timezone, + 'created_at' => $this->created_at, + 'is_production_ready' => $this->is_production_ready, + 'pending_steps' => $this->pending_steps, + 'legal' => $this->legal, + 'customization' => $this->customization, + 'certificate' => $this->certificate, + 'fiel' => $this->fiel, + 'receipts' => $this->receipts, + 'self_invoice' => $this->self_invoice + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/OrganizationAddress.php b/src/Models/OrganizationAddress.php new file mode 100644 index 0000000..dff9be0 --- /dev/null +++ b/src/Models/OrganizationAddress.php @@ -0,0 +1,48 @@ + $this->street, + 'exterior' => $this->exterior, + 'interior' => $this->interior, + 'neighborhood' => $this->neighborhood, + 'city' => $this->city, + 'municipality' => $this->municipality, + 'zip' => $this->zip, + 'state' => $this->state + ], fn (?string $v): bool => $v !== null); + } +} diff --git a/src/Models/OrganizationCertsInput.php b/src/Models/OrganizationCertsInput.php new file mode 100644 index 0000000..0b3e86c --- /dev/null +++ b/src/Models/OrganizationCertsInput.php @@ -0,0 +1,33 @@ + $this->cer, + 'key' => $this->key, + 'password' => $this->password + ], fn (string $v): true => $v !== null); + } +} diff --git a/src/Models/OrganizationCreateInput.php b/src/Models/OrganizationCreateInput.php new file mode 100644 index 0000000..baba5e1 --- /dev/null +++ b/src/Models/OrganizationCreateInput.php @@ -0,0 +1,27 @@ + $this->name + ], fn (string $v): true => $v !== null); + } +} diff --git a/src/Models/OrganizationCustomizationInput.php b/src/Models/OrganizationCustomizationInput.php new file mode 100644 index 0000000..491c9bd --- /dev/null +++ b/src/Models/OrganizationCustomizationInput.php @@ -0,0 +1,36 @@ + $this->color, + 'next_folio_number' => $this->next_folio_number, + 'next_folio_number_test' => $this->next_folio_number_test, + 'pdf_extra' => $this->pdf_extra + ], fn (string|int|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/OrganizationDeleteCerts.php b/src/Models/OrganizationDeleteCerts.php new file mode 100644 index 0000000..aa8c0e5 --- /dev/null +++ b/src/Models/OrganizationDeleteCerts.php @@ -0,0 +1,27 @@ + $this->updated_at + ], fn (?string $v): bool => $v !== null); + } +} diff --git a/src/Models/OrganizationDomainInput.php b/src/Models/OrganizationDomainInput.php new file mode 100644 index 0000000..03014df --- /dev/null +++ b/src/Models/OrganizationDomainInput.php @@ -0,0 +1,27 @@ + $this->domain?->toArray() + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/OrganizationFielInput.php b/src/Models/OrganizationFielInput.php new file mode 100644 index 0000000..5da8161 --- /dev/null +++ b/src/Models/OrganizationFielInput.php @@ -0,0 +1,33 @@ + $this->cer, + 'key' => $this->key, + 'password' => $this->password + ], fn (string $v): true => $v !== null); + } +} diff --git a/src/Models/OrganizationInvite.php b/src/Models/OrganizationInvite.php new file mode 100644 index 0000000..c996475 --- /dev/null +++ b/src/Models/OrganizationInvite.php @@ -0,0 +1,48 @@ + $this->id, + 'created_at' => $this->created_at, + 'email' => $this->email, + 'organization_name' => $this->organization_name, + 'role' => $this->role, + 'role_name' => $this->role_name, + 'roles' => $this->roles, + 'expires_at' => $this->expires_at + ], fn (string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/OrganizationInviteCreateInput.php b/src/Models/OrganizationInviteCreateInput.php new file mode 100644 index 0000000..054410e --- /dev/null +++ b/src/Models/OrganizationInviteCreateInput.php @@ -0,0 +1,30 @@ + $this->email, + 'role' => $this->role + ], fn (?string $v): bool => $v !== null); + } +} diff --git a/src/Models/OrganizationInviteRespondInput.php b/src/Models/OrganizationInviteRespondInput.php new file mode 100644 index 0000000..21d06a1 --- /dev/null +++ b/src/Models/OrganizationInviteRespondInput.php @@ -0,0 +1,27 @@ + $this->accept + ], fn (bool $v): true => $v !== null); + } +} diff --git a/src/Models/OrganizationLegalInput.php b/src/Models/OrganizationLegalInput.php new file mode 100644 index 0000000..3db1161 --- /dev/null +++ b/src/Models/OrganizationLegalInput.php @@ -0,0 +1,45 @@ + $this->name, + 'legal_name' => $this->legal_name, + 'tax_system' => $this->tax_system, + 'website' => $this->website, + 'support_email' => $this->support_email, + 'phone' => $this->phone, + 'address' => $this->address + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/OrganizationLogoInput.php b/src/Models/OrganizationLogoInput.php new file mode 100644 index 0000000..47261a4 --- /dev/null +++ b/src/Models/OrganizationLogoInput.php @@ -0,0 +1,27 @@ + $this->file + ], fn (string $v): true => $v !== null); + } +} diff --git a/src/Models/OrganizationPermissionRole.php b/src/Models/OrganizationPermissionRole.php new file mode 100644 index 0000000..a4681c5 --- /dev/null +++ b/src/Models/OrganizationPermissionRole.php @@ -0,0 +1,54 @@ + $this->id, + 'name' => $this->name, + 'template_code' => $this->template_code, + 'organization' => $this->organization, + 'used_by' => $this->used_by, + 'overrides_add' => $this->overrides_add, + 'overrides_remove' => $this->overrides_remove, + 'operations' => $this->operations, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at + ], fn (int|string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/OrganizationPermissionRoleCreateInput.php b/src/Models/OrganizationPermissionRoleCreateInput.php new file mode 100644 index 0000000..3a5e6d3 --- /dev/null +++ b/src/Models/OrganizationPermissionRoleCreateInput.php @@ -0,0 +1,36 @@ + $this->name, + 'template_code' => $this->template_code, + 'add' => $this->add, + 'remove' => $this->remove + ], fn (string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/OrganizationPermissionRoleTemplate.php b/src/Models/OrganizationPermissionRoleTemplate.php new file mode 100644 index 0000000..98675f7 --- /dev/null +++ b/src/Models/OrganizationPermissionRoleTemplate.php @@ -0,0 +1,33 @@ + $this->code, + 'label' => $this->label, + 'operations' => $this->operations + ], fn (string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/OrganizationPermissionRoleUpdateInput.php b/src/Models/OrganizationPermissionRoleUpdateInput.php new file mode 100644 index 0000000..e8e7146 --- /dev/null +++ b/src/Models/OrganizationPermissionRoleUpdateInput.php @@ -0,0 +1,36 @@ + $this->name, + 'template_code' => $this->template_code, + 'add' => $this->add, + 'remove' => $this->remove + ], fn (string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/OrganizationReceiptsInput.php b/src/Models/OrganizationReceiptsInput.php new file mode 100644 index 0000000..646cd4d --- /dev/null +++ b/src/Models/OrganizationReceiptsInput.php @@ -0,0 +1,42 @@ + $this->periodicity, + 'duration_days' => $this->duration_days, + 'next_folio_number' => $this->next_folio_number, + 'next_folio_number_test' => $this->next_folio_number_test, + 'activate_global_invoice' => $this->activate_global_invoice, + 'grouped_items_invoice' => $this->grouped_items_invoice + ], fn (string|int|bool|null $v): bool => $v !== null); + } +} diff --git a/src/Models/OrganizationSearchResult.php b/src/Models/OrganizationSearchResult.php new file mode 100644 index 0000000..c4aa9b7 --- /dev/null +++ b/src/Models/OrganizationSearchResult.php @@ -0,0 +1,47 @@ + is_array($i) ? \Facturapi\Models\Organization::fromArray($i) : $i, $data['data']) : [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'page' => $this->page, + 'total_pages' => $this->total_pages, + 'total_results' => $this->total_results, + 'previous_cursor' => $this->previous_cursor, + 'next_cursor' => $this->next_cursor, + 'totals_are_capped' => $this->totals_are_capped, + 'data' => $this->data !== null ? array_map(fn (\Facturapi\Models\Organization $i): array => $i->toArray(), $this->data) : null + ], fn (int|string|bool|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/OrganizationSelfInvoiceInput.php b/src/Models/OrganizationSelfInvoiceInput.php new file mode 100644 index 0000000..66b7230 --- /dev/null +++ b/src/Models/OrganizationSelfInvoiceInput.php @@ -0,0 +1,33 @@ + $this->allowed_cfdi_uses, + 'apply_resico_isr' => $this->apply_resico_isr, + 'support_email' => $this->support_email + ], fn (bool|string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/OrganizationSeriesCreateInput.php b/src/Models/OrganizationSeriesCreateInput.php new file mode 100644 index 0000000..e3a5bc2 --- /dev/null +++ b/src/Models/OrganizationSeriesCreateInput.php @@ -0,0 +1,33 @@ + $this->series, + 'next_folio' => $this->next_folio, + 'next_folio_test' => $this->next_folio_test + ], fn (string|int $v): true => $v !== null); + } +} diff --git a/src/Models/OrganizationSeriesDefaultInput.php b/src/Models/OrganizationSeriesDefaultInput.php new file mode 100644 index 0000000..d5dc95d --- /dev/null +++ b/src/Models/OrganizationSeriesDefaultInput.php @@ -0,0 +1,30 @@ + $this->type, + 'series' => $this->series + ], fn (string $v): true => $v !== null); + } +} diff --git a/src/Models/OrganizationSeriesGroup.php b/src/Models/OrganizationSeriesGroup.php new file mode 100644 index 0000000..853f0a7 --- /dev/null +++ b/src/Models/OrganizationSeriesGroup.php @@ -0,0 +1,33 @@ + $this->series, + 'next_folio' => $this->next_folio, + 'next_folio_test' => $this->next_folio_test + ], fn (string|int|null $v): bool => $v !== null); + } +} diff --git a/src/Models/OrganizationSeriesUpdateInput.php b/src/Models/OrganizationSeriesUpdateInput.php new file mode 100644 index 0000000..b1d8c96 --- /dev/null +++ b/src/Models/OrganizationSeriesUpdateInput.php @@ -0,0 +1,30 @@ + $this->next_folio, + 'next_folio_test' => $this->next_folio_test + ], fn (?int $v): bool => $v !== null); + } +} diff --git a/src/Models/OrganizationUserAccess.php b/src/Models/OrganizationUserAccess.php new file mode 100644 index 0000000..de50f04 --- /dev/null +++ b/src/Models/OrganizationUserAccess.php @@ -0,0 +1,51 @@ + $this->id, + 'full_name' => $this->full_name, + 'email' => $this->email, + 'role' => $this->role, + 'role_name' => $this->role_name, + 'organization' => $this->organization, + 'operations' => $this->operations, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at + ], fn (string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/OrganizationUserAccessRoleUpdateInput.php b/src/Models/OrganizationUserAccessRoleUpdateInput.php new file mode 100644 index 0000000..938e62d --- /dev/null +++ b/src/Models/OrganizationUserAccessRoleUpdateInput.php @@ -0,0 +1,27 @@ + $this->role + ], fn (string $v): true => $v !== null); + } +} diff --git a/src/Models/PagoComplementInput.php b/src/Models/PagoComplementInput.php new file mode 100644 index 0000000..b021bb2 --- /dev/null +++ b/src/Models/PagoComplementInput.php @@ -0,0 +1,30 @@ + $this->type, + 'data' => $this->data?->toArray() + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/PagoComplementProperties.php b/src/Models/PagoComplementProperties.php new file mode 100644 index 0000000..97c2839 --- /dev/null +++ b/src/Models/PagoComplementProperties.php @@ -0,0 +1,30 @@ + $this->type, + 'data' => $this->data?->toArray() + ], fn (string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/PagoOrCustomComplementInput.php b/src/Models/PagoOrCustomComplementInput.php new file mode 100644 index 0000000..a74b08f --- /dev/null +++ b/src/Models/PagoOrCustomComplementInput.php @@ -0,0 +1,27 @@ + $this->type + ], fn (string $v): true => $v !== null); + } +} diff --git a/src/Models/PagoOrCustomComplementProperties.php b/src/Models/PagoOrCustomComplementProperties.php new file mode 100644 index 0000000..1b6c5b2 --- /dev/null +++ b/src/Models/PagoOrCustomComplementProperties.php @@ -0,0 +1,27 @@ + $this->type + ], fn (?string $v): bool => $v !== null); + } +} diff --git a/src/Models/PartInput.php b/src/Models/PartInput.php new file mode 100644 index 0000000..9d2a6ee --- /dev/null +++ b/src/Models/PartInput.php @@ -0,0 +1,45 @@ + $this->description, + 'product_key' => $this->product_key, + 'quantity' => $this->quantity, + 'sku' => $this->sku, + 'unit_price' => $this->unit_price, + 'unit_name' => $this->unit_name, + 'customs_keys' => $this->customs_keys + ], fn (float|string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/Parts.php b/src/Models/Parts.php new file mode 100644 index 0000000..be43183 --- /dev/null +++ b/src/Models/Parts.php @@ -0,0 +1,45 @@ + $this->description, + 'product_key' => $this->product_key, + 'quantity' => $this->quantity, + 'sku' => $this->sku, + 'unit_price' => $this->unit_price, + 'unit_name' => $this->unit_name, + 'customs_keys' => $this->customs_keys + ], fn (float|string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/PaymentInput.php b/src/Models/PaymentInput.php new file mode 100644 index 0000000..0649694 --- /dev/null +++ b/src/Models/PaymentInput.php @@ -0,0 +1,69 @@ + $this->payment_form, + 'related_documents' => $this->related_documents, + 'currency' => $this->currency, + 'exchange' => $this->exchange, + 'date' => $this->date, + 'numOperacion' => $this->numOperacion, + 'rfcEmisorCtaOrd' => $this->rfcEmisorCtaOrd, + 'nomBancoOrdExt' => $this->nomBancoOrdExt, + 'ctaOrdenante' => $this->ctaOrdenante, + 'rfcEmisorCtaBen' => $this->rfcEmisorCtaBen, + 'ctaBeneficiario' => $this->ctaBeneficiario, + 'tipoCadPago' => $this->tipoCadPago, + 'certPago' => $this->certPago, + 'cadPago' => $this->cadPago, + 'selloPago' => $this->selloPago + ], fn (float|string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/PaymentSummary.php b/src/Models/PaymentSummary.php new file mode 100644 index 0000000..baf103e --- /dev/null +++ b/src/Models/PaymentSummary.php @@ -0,0 +1,51 @@ + $this->uuid, + 'installment' => $this->installment, + 'last_balance' => $this->last_balance, + 'total' => $this->total, + 'currency' => $this->currency, + 'amount' => $this->amount, + 'taxes' => $this->taxes, + 'folio_number' => $this->folio_number, + 'series' => $this->series, + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/Product.php b/src/Models/Product.php new file mode 100644 index 0000000..e27336e --- /dev/null +++ b/src/Models/Product.php @@ -0,0 +1,71 @@ + $i instanceof \Facturapi\Models\BaseTax ? $i : \Facturapi\Models\BaseTax::fromArray((array) $i), $data['taxes']) + : null, + isset($data['local_taxes']) && is_array($data['local_taxes']) + ? array_map(fn (mixed $i): \Facturapi\Models\LocalTax => $i instanceof \Facturapi\Models\LocalTax ? $i : \Facturapi\Models\LocalTax::fromArray((array) $i), $data['local_taxes']) + : null, + isset($data['unit_key']) ? (string) $data['unit_key'] : '', + isset($data['unit_name']) ? (string) $data['unit_name'] : null, + isset($data['sku']) ? (string) $data['sku'] : null + ); + } + + public function toArray(): array + { + return array_filter([ + 'id' => $this->id, + 'created_at' => $this->created_at, + 'livemode' => $this->livemode, + 'description' => $this->description, + 'product_key' => $this->product_key, + 'price' => $this->price, + 'tax_included' => $this->tax_included, + 'taxability' => $this->taxability, + 'taxes' => $this->taxes !== null ? array_map(fn (\Facturapi\Models\BaseTax $i): array => $i->toArray(), $this->taxes) : null, + 'local_taxes' => $this->local_taxes !== null ? array_map(fn (\Facturapi\Models\LocalTax $i): array => $i->toArray(), $this->local_taxes) : null, + 'unit_key' => $this->unit_key, + 'unit_name' => $this->unit_name, + 'sku' => $this->sku, + ], fn (int|float|bool|string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/ProductCatalogResult.php b/src/Models/ProductCatalogResult.php new file mode 100644 index 0000000..3be6da4 --- /dev/null +++ b/src/Models/ProductCatalogResult.php @@ -0,0 +1,33 @@ + $this->key, + 'description' => $this->description, + 'score' => $this->score + ], fn (string|float|null $v): bool => $v !== null); + } +} diff --git a/src/Models/ProductCatalogSearchResult.php b/src/Models/ProductCatalogSearchResult.php new file mode 100644 index 0000000..2af3475 --- /dev/null +++ b/src/Models/ProductCatalogSearchResult.php @@ -0,0 +1,47 @@ + is_array($i) ? \Facturapi\Models\ProductCatalogResult::fromArray($i) : $i, $data['data']) : [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'page' => $this->page, + 'total_pages' => $this->total_pages, + 'total_results' => $this->total_results, + 'previous_cursor' => $this->previous_cursor, + 'next_cursor' => $this->next_cursor, + 'totals_are_capped' => $this->totals_are_capped, + 'data' => $this->data !== null ? array_map(fn (\Facturapi\Models\ProductCatalogResult $i): array => $i->toArray(), $this->data) : null + ], fn (int|string|bool|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/ProductEgresoProperties.php b/src/Models/ProductEgresoProperties.php new file mode 100644 index 0000000..7c67f20 --- /dev/null +++ b/src/Models/ProductEgresoProperties.php @@ -0,0 +1,55 @@ + is_array($i) ? \Facturapi\Models\BaseTax::fromArray($i) : $i, $data['taxes']) : [], + isset($data['local_taxes']) && is_array($data['local_taxes']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\LocalTax::fromArray($i) : $i, $data['local_taxes']) : [], + $data['unit_key'] ?? null, + $data['unit_name'] ?? null + ); + } + + public function toArray(): array + { + return array_filter([ + 'description' => $this->description, + 'product_key' => $this->product_key, + 'price' => $this->price, + 'tax_included' => $this->tax_included, + 'taxability' => $this->taxability, + 'taxes' => $this->taxes !== null ? array_map(fn (\Facturapi\Models\BaseTax $i): array => $i->toArray(), $this->taxes) : null, + 'local_taxes' => $this->local_taxes !== null ? array_map(fn (\Facturapi\Models\LocalTax $i): array => $i->toArray(), $this->local_taxes) : null, + 'unit_key' => $this->unit_key, + 'unit_name' => $this->unit_name + ], fn (float|bool|string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/ProductProperties.php b/src/Models/ProductProperties.php new file mode 100644 index 0000000..2bb5326 --- /dev/null +++ b/src/Models/ProductProperties.php @@ -0,0 +1,58 @@ + is_array($i) ? \Facturapi\Models\BaseTax::fromArray($i) : $i, $data['taxes']) : [], + isset($data['local_taxes']) && is_array($data['local_taxes']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\LocalTax::fromArray($i) : $i, $data['local_taxes']) : [], + $data['unit_key'] ?? '', + $data['unit_name'] ?? null, + $data['sku'] ?? null + ); + } + + public function toArray(): array + { + return array_filter([ + 'description' => $this->description, + 'product_key' => $this->product_key, + 'price' => $this->price, + 'tax_included' => $this->tax_included, + 'taxability' => $this->taxability, + 'taxes' => $this->taxes !== null ? array_map(fn (\Facturapi\Models\BaseTax $i): array => $i->toArray(), $this->taxes) : null, + 'local_taxes' => $this->local_taxes !== null ? array_map(fn (\Facturapi\Models\LocalTax $i): array => $i->toArray(), $this->local_taxes) : null, + 'unit_key' => $this->unit_key, + 'unit_name' => $this->unit_name, + 'sku' => $this->sku + ], fn (float|bool|string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/ProductSearchResult.php b/src/Models/ProductSearchResult.php new file mode 100644 index 0000000..f01cc28 --- /dev/null +++ b/src/Models/ProductSearchResult.php @@ -0,0 +1,49 @@ + $i instanceof \Facturapi\Models\Product ? $i : \Facturapi\Models\Product::fromArray((array) $i), $data['data']) + : [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'page' => $this->page, + 'total_pages' => $this->total_pages, + 'total_results' => $this->total_results, + 'previous_cursor' => $this->previous_cursor, + 'next_cursor' => $this->next_cursor, + 'totals_are_capped' => $this->totals_are_capped, + 'data' => $this->data !== null ? array_map(fn (\Facturapi\Models\Product $i): array => $i->toArray(), $this->data) : null, + ], fn (int|string|bool|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/Receipt.php b/src/Models/Receipt.php new file mode 100644 index 0000000..4fb4003 --- /dev/null +++ b/src/Models/Receipt.php @@ -0,0 +1,83 @@ + is_array($i) ? \Facturapi\Models\LineItem::fromArray($i) : $i, $data['items']) : [], + $data['external_id'] ?? null, + $data['idempotency_key'] ?? null, + $data['payment_form'] ?? null, + $data['folio_number'] ?? 0, + $data['currency'] ?? null, + $data['exchange'] ?? 0, + $data['branch'] ?? null + ); + } + + public function toArray(): array + { + return array_filter([ + 'id' => $this->id, + 'created_at' => $this->created_at, + 'livemode' => $this->livemode, + 'date' => $this->date, + 'expires_at' => $this->expires_at, + 'status' => $this->status, + 'self_invoice_url' => $this->self_invoice_url, + 'total' => $this->total, + 'invoice' => $this->invoice, + 'customer' => $this->customer, + 'key' => $this->key, + 'items' => $this->items !== null ? array_map(fn (\Facturapi\Models\LineItem $i): array => $i->toArray(), $this->items) : null, + 'external_id' => $this->external_id, + 'idempotency_key' => $this->idempotency_key, + 'payment_form' => $this->payment_form, + 'folio_number' => $this->folio_number, + 'currency' => $this->currency, + 'exchange' => $this->exchange, + 'branch' => $this->branch + ], fn (string|bool|float|int|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/ReceiptAssignCustomerInput.php b/src/Models/ReceiptAssignCustomerInput.php new file mode 100644 index 0000000..722da00 --- /dev/null +++ b/src/Models/ReceiptAssignCustomerInput.php @@ -0,0 +1,27 @@ + $this->customer + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/ReceiptEditableProperties.php b/src/Models/ReceiptEditableProperties.php new file mode 100644 index 0000000..bf37c02 --- /dev/null +++ b/src/Models/ReceiptEditableProperties.php @@ -0,0 +1,45 @@ + $this->date, + 'payment_form' => $this->payment_form, + 'folio_number' => $this->folio_number, + 'currency' => $this->currency, + 'exchange' => $this->exchange, + 'branch' => $this->branch, + 'external_id' => $this->external_id + ], fn (int|float|string|null $v): bool => $v !== null); + } +} diff --git a/src/Models/ReceiptInput.php b/src/Models/ReceiptInput.php new file mode 100644 index 0000000..79cc1b2 --- /dev/null +++ b/src/Models/ReceiptInput.php @@ -0,0 +1,59 @@ + is_array($i) ? \Facturapi\Models\LineItemInput::fromArray($i) : $i, $data['items']) : [], + $data['date'] ?? null, + $data['payment_form'] ?? '', + $data['folio_number'] ?? 0, + $data['currency'] ?? null, + $data['exchange'] ?? 0, + $data['branch'] ?? null, + $data['external_id'] ?? null, + $data['idempotency_key'] ?? null + ); + } + + public function toArray(): array + { + return array_filter([ + 'customer' => $this->customer, + 'address' => $this->address, + 'items' => $this->items !== null ? array_map(fn (\Facturapi\Models\LineItemInput $i): array => $i->toArray(), $this->items) : null, + 'date' => $this->date, + 'payment_form' => $this->payment_form, + 'folio_number' => $this->folio_number, + 'currency' => $this->currency, + 'exchange' => $this->exchange, + 'branch' => $this->branch, + 'external_id' => $this->external_id, + 'idempotency_key' => $this->idempotency_key + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/ReceiptProperties.php b/src/Models/ReceiptProperties.php new file mode 100644 index 0000000..2c139f1 --- /dev/null +++ b/src/Models/ReceiptProperties.php @@ -0,0 +1,74 @@ + is_array($i) ? \Facturapi\Models\LineItem::fromArray($i) : $i, $data['items']) : [], + $data['external_id'] ?? null, + $data['idempotency_key'] ?? null, + $data['payment_form'] ?? null, + $data['folio_number'] ?? 0, + $data['currency'] ?? null, + $data['exchange'] ?? 0, + $data['branch'] ?? null + ); + } + + public function toArray(): array + { + return array_filter([ + 'date' => $this->date, + 'expires_at' => $this->expires_at, + 'status' => $this->status, + 'self_invoice_url' => $this->self_invoice_url, + 'total' => $this->total, + 'invoice' => $this->invoice, + 'customer' => $this->customer, + 'key' => $this->key, + 'items' => $this->items !== null ? array_map(fn (\Facturapi\Models\LineItem $i): array => $i->toArray(), $this->items) : null, + 'external_id' => $this->external_id, + 'idempotency_key' => $this->idempotency_key, + 'payment_form' => $this->payment_form, + 'folio_number' => $this->folio_number, + 'currency' => $this->currency, + 'exchange' => $this->exchange, + 'branch' => $this->branch + ], fn (int|float|string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/ReceiptSearchResult.php b/src/Models/ReceiptSearchResult.php new file mode 100644 index 0000000..3c20c80 --- /dev/null +++ b/src/Models/ReceiptSearchResult.php @@ -0,0 +1,47 @@ + is_array($i) ? \Facturapi\Models\Receipt::fromArray($i) : $i, $data['data']) : [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'page' => $this->page, + 'total_pages' => $this->total_pages, + 'total_results' => $this->total_results, + 'previous_cursor' => $this->previous_cursor, + 'next_cursor' => $this->next_cursor, + 'totals_are_capped' => $this->totals_are_capped, + 'data' => $this->data !== null ? array_map(fn (\Facturapi\Models\Receipt $i): array => $i->toArray(), $this->data) : null + ], fn (int|string|bool|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/RelatedDocument.php b/src/Models/RelatedDocument.php new file mode 100644 index 0000000..393d063 --- /dev/null +++ b/src/Models/RelatedDocument.php @@ -0,0 +1,30 @@ + $this->relationship, + 'documents' => $this->documents + ], fn (string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/RelatedDocumentInput.php b/src/Models/RelatedDocumentInput.php new file mode 100644 index 0000000..0a083f0 --- /dev/null +++ b/src/Models/RelatedDocumentInput.php @@ -0,0 +1,30 @@ + $this->relationship, + 'documents' => $this->documents + ], fn (string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/RelatedResourceMessage.php b/src/Models/RelatedResourceMessage.php new file mode 100644 index 0000000..41fed47 --- /dev/null +++ b/src/Models/RelatedResourceMessage.php @@ -0,0 +1,42 @@ + $this->resource_type, + 'resource_id' => $this->resource_id, + 'source' => $this->source, + 'severity' => $this->severity, + 'message' => $this->message, + 'created_at' => $this->created_at + ], fn (?string $v): bool => $v !== null); + } +} diff --git a/src/Models/ResourceAutoGeneratedProps.php b/src/Models/ResourceAutoGeneratedProps.php new file mode 100644 index 0000000..b0cd8e7 --- /dev/null +++ b/src/Models/ResourceAutoGeneratedProps.php @@ -0,0 +1,33 @@ + $this->id, + 'created_at' => $this->created_at, + 'livemode' => $this->livemode + ], fn (string|bool $v): true => $v !== null); + } +} diff --git a/src/Models/Retention.php b/src/Models/Retention.php new file mode 100644 index 0000000..61744e7 --- /dev/null +++ b/src/Models/Retention.php @@ -0,0 +1,94 @@ + is_array($i) ? \Facturapi\Models\CustomComplementData::fromArray($i) : $i, $data['complements']) : [], + $data['pdf_custom_section'] ?? null, + $data['addenda'] ?? null, + isset($data['namespaces']) && is_array($data['namespaces']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\NamespaceProperties::fromArray($i) : $i, $data['namespaces']) : [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'id' => $this->id, + 'created_at' => $this->created_at, + 'livemode' => $this->livemode, + 'status' => $this->status, + 'verification_url' => $this->verification_url, + 'type' => $this->type, + 'uuid' => $this->uuid, + 'stamp' => $this->stamp?->toArray(), + 'customer' => $this->customer?->toArray(), + 'is_ready_to_stamp' => $this->is_ready_to_stamp, + 'cve_retenc' => $this->cve_retenc, + 'fecha_exp' => $this->fecha_exp, + 'desc_retenc' => $this->desc_retenc, + 'folio_int' => $this->folio_int, + 'periodo' => $this->periodo, + 'totales' => $this->totales, + 'external_id' => $this->external_id, + 'idempotency_key' => $this->idempotency_key, + 'complements' => $this->complements !== null ? array_map(fn (\Facturapi\Models\CustomComplementData $i) => $i->toArray(), $this->complements) : null, + 'pdf_custom_section' => $this->pdf_custom_section, + 'addenda' => $this->addenda, + 'namespaces' => $this->namespaces !== null ? array_map(fn (\Facturapi\Models\NamespaceProperties $i): array => $i->toArray(), $this->namespaces) : null + ], fn (bool|string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/RetentionProperties.php b/src/Models/RetentionProperties.php new file mode 100644 index 0000000..7f6288f --- /dev/null +++ b/src/Models/RetentionProperties.php @@ -0,0 +1,64 @@ + is_array($i) ? \Facturapi\Models\CustomComplementData::fromArray($i) : $i, $data['complements']) : [], + $data['pdf_custom_section'] ?? null, + $data['addenda'] ?? null, + isset($data['namespaces']) && is_array($data['namespaces']) ? array_map(fn ($i): mixed => is_array($i) ? \Facturapi\Models\NamespaceProperties::fromArray($i) : $i, $data['namespaces']) : [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'cve_retenc' => $this->cve_retenc, + 'fecha_exp' => $this->fecha_exp, + 'desc_retenc' => $this->desc_retenc, + 'folio_int' => $this->folio_int, + 'periodo' => $this->periodo, + 'totales' => $this->totales, + 'external_id' => $this->external_id, + 'idempotency_key' => $this->idempotency_key, + 'complements' => $this->complements !== null ? array_map(fn (\Facturapi\Models\CustomComplementData $i) => $i->toArray(), $this->complements) : null, + 'pdf_custom_section' => $this->pdf_custom_section, + 'addenda' => $this->addenda, + 'namespaces' => $this->namespaces !== null ? array_map(fn (\Facturapi\Models\NamespaceProperties $i): array => $i->toArray(), $this->namespaces) : null + ], fn (string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/RetentionReadOnlyProperties.php b/src/Models/RetentionReadOnlyProperties.php new file mode 100644 index 0000000..b356ede --- /dev/null +++ b/src/Models/RetentionReadOnlyProperties.php @@ -0,0 +1,45 @@ + $this->status, + 'verification_url' => $this->verification_url, + 'type' => $this->type, + 'uuid' => $this->uuid, + 'stamp' => $this->stamp?->toArray(), + 'customer' => $this->customer?->toArray(), + 'is_ready_to_stamp' => $this->is_ready_to_stamp + ], fn (string|bool|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/RetentionSearchResult.php b/src/Models/RetentionSearchResult.php new file mode 100644 index 0000000..0de6afd --- /dev/null +++ b/src/Models/RetentionSearchResult.php @@ -0,0 +1,47 @@ + is_array($i) ? \Facturapi\Models\Retention::fromArray($i) : $i, $data['data']) : [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'page' => $this->page, + 'total_pages' => $this->total_pages, + 'total_results' => $this->total_results, + 'previous_cursor' => $this->previous_cursor, + 'next_cursor' => $this->next_cursor, + 'totals_are_capped' => $this->totals_are_capped, + 'data' => $this->data !== null ? array_map(fn (\Facturapi\Models\Retention $i): array => $i->toArray(), $this->data) : null + ], fn (int|string|bool|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/RetentionUpdateInput.php b/src/Models/RetentionUpdateInput.php new file mode 100644 index 0000000..9c232d8 --- /dev/null +++ b/src/Models/RetentionUpdateInput.php @@ -0,0 +1,68 @@ + is_array($i) ? \Facturapi\Models\CustomComplementData::fromArray($i) : $i, $data['complements']) : [], + $data['pdf_custom_section'] ?? null, + $data['addenda'] ?? null, + $data['namespaces'] ?? [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'status' => $this->status, + 'customer' => $this->customer, + 'cve_retenc' => $this->cve_retenc, + 'fecha_exp' => $this->fecha_exp, + 'desc_retenc' => $this->desc_retenc, + 'folio_int' => $this->folio_int, + 'periodo' => $this->periodo, + 'totales' => $this->totales, + 'external_id' => $this->external_id, + 'idempotency_key' => $this->idempotency_key, + 'complements' => $this->complements !== null ? array_map(fn (\Facturapi\Models\CustomComplementData $i) => $i->toArray(), $this->complements) : null, + 'pdf_custom_section' => $this->pdf_custom_section, + 'addenda' => $this->addenda, + 'namespaces' => $this->namespaces + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/SearchKeyDescriptionResult.php b/src/Models/SearchKeyDescriptionResult.php new file mode 100644 index 0000000..a9b7f56 --- /dev/null +++ b/src/Models/SearchKeyDescriptionResult.php @@ -0,0 +1,45 @@ + $this->page, + 'total_pages' => $this->total_pages, + 'total_results' => $this->total_results, + 'previous_cursor' => $this->previous_cursor, + 'next_cursor' => $this->next_cursor, + 'totals_are_capped' => $this->totals_are_capped, + 'data' => $this->data + ], fn (int|string|bool|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/SearchResult.php b/src/Models/SearchResult.php new file mode 100644 index 0000000..c0a81d0 --- /dev/null +++ b/src/Models/SearchResult.php @@ -0,0 +1,42 @@ + $this->page, + 'total_pages' => $this->total_pages, + 'total_results' => $this->total_results, + 'previous_cursor' => $this->previous_cursor, + 'next_cursor' => $this->next_cursor, + 'totals_are_capped' => $this->totals_are_capped + ], fn (int|string|bool|null $v): bool => $v !== null); + } +} diff --git a/src/Models/SignedDownloadUrl.php b/src/Models/SignedDownloadUrl.php new file mode 100644 index 0000000..6fc76a6 --- /dev/null +++ b/src/Models/SignedDownloadUrl.php @@ -0,0 +1,36 @@ + $this->url, + 'expires_at' => $this->expires_at, + 'content_type' => $this->content_type, + 'filename' => $this->filename + ], fn (string $v): true => $v !== null); + } +} diff --git a/src/Models/Stamp.php b/src/Models/Stamp.php new file mode 100644 index 0000000..00b33f5 --- /dev/null +++ b/src/Models/Stamp.php @@ -0,0 +1,36 @@ + $this->signature, + 'date' => $this->date, + 'sat_cert_number' => $this->sat_cert_number, + 'sat_signature' => $this->sat_signature + ], fn (?string $v): bool => $v !== null); + } +} diff --git a/src/Models/SuccessResponse.php b/src/Models/SuccessResponse.php new file mode 100644 index 0000000..6308d38 --- /dev/null +++ b/src/Models/SuccessResponse.php @@ -0,0 +1,34 @@ +success = $success; + $this->ok = $ok ?? $success; + } + + public static function fromArray(array $data): self + { + $val = (bool) ($data['success'] ?? $data['ok'] ?? false); + + return new self($val, $val); + } + + public function toArray(): array + { + return [ + 'success' => $this->success, + 'ok' => $this->ok, + ]; + } +} diff --git a/src/Models/TaxIdValidationResult.php b/src/Models/TaxIdValidationResult.php new file mode 100644 index 0000000..1b9de69 --- /dev/null +++ b/src/Models/TaxIdValidationResult.php @@ -0,0 +1,40 @@ +|null $efos + * @param array>|null $errors + */ + public function __construct( + public bool $is_valid, + public ?array $efos = null, + public ?array $errors = null, + ) { + } + + /** + * @param array $data + */ + public static function fromArray(array $data): self + { + return new self( + $data['is_valid'] ?? false, + $data['efos'] ?? null, + $data['errors'] ?? null + ); + } + + public function toArray(): array + { + return array_filter([ + 'is_valid' => $this->is_valid, + 'efos' => $this->efos, + 'errors' => $this->errors, + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/ThirdParty.php b/src/Models/ThirdParty.php new file mode 100644 index 0000000..eb7abb2 --- /dev/null +++ b/src/Models/ThirdParty.php @@ -0,0 +1,36 @@ + $this->legal_name, + 'tax_id' => $this->tax_id, + 'tax_system' => $this->tax_system, + 'zip' => $this->zip + ], fn (?string $v): bool => $v !== null); + } +} diff --git a/src/Models/ToInvoiceInput.php b/src/Models/ToInvoiceInput.php new file mode 100644 index 0000000..7248a27 --- /dev/null +++ b/src/Models/ToInvoiceInput.php @@ -0,0 +1,39 @@ + $this->keys, + 'customer' => $this->customer, + 'use' => $this->use, + 'dry_run' => $this->dry_run, + 'payment_form' => $this->payment_form + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/ToInvoicePreviewInput.php b/src/Models/ToInvoicePreviewInput.php new file mode 100644 index 0000000..9c9bd65 --- /dev/null +++ b/src/Models/ToInvoicePreviewInput.php @@ -0,0 +1,33 @@ + $this->keys, + 'customer' => $this->customer, + 'use' => $this->use + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/UnitCatalogResult.php b/src/Models/UnitCatalogResult.php new file mode 100644 index 0000000..b065e25 --- /dev/null +++ b/src/Models/UnitCatalogResult.php @@ -0,0 +1,33 @@ + $this->key, + 'description' => $this->description, + 'score' => $this->score + ], fn (string|float|null $v): bool => $v !== null); + } +} diff --git a/src/Models/UnitCatalogSearchResult.php b/src/Models/UnitCatalogSearchResult.php new file mode 100644 index 0000000..fae2f99 --- /dev/null +++ b/src/Models/UnitCatalogSearchResult.php @@ -0,0 +1,47 @@ + is_array($i) ? \Facturapi\Models\UnitCatalogResult::fromArray($i) : $i, $data['data']) : [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'page' => $this->page, + 'total_pages' => $this->total_pages, + 'total_results' => $this->total_results, + 'previous_cursor' => $this->previous_cursor, + 'next_cursor' => $this->next_cursor, + 'totals_are_capped' => $this->totals_are_capped, + 'data' => $this->data !== null ? array_map(fn (\Facturapi\Models\UnitCatalogResult $i): array => $i->toArray(), $this->data) : null + ], fn (int|string|bool|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/Webhook.php b/src/Models/Webhook.php new file mode 100644 index 0000000..f4b6143 --- /dev/null +++ b/src/Models/Webhook.php @@ -0,0 +1,45 @@ + $this->id, + 'created_at' => $this->created_at, + 'livemode' => $this->livemode, + 'organization' => $this->organization, + 'enabled_events' => $this->enabled_events, + 'url' => $this->url, + 'status' => $this->status + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/WebhookCreateEdit.php b/src/Models/WebhookCreateEdit.php new file mode 100644 index 0000000..90f16ae --- /dev/null +++ b/src/Models/WebhookCreateEdit.php @@ -0,0 +1,30 @@ + $this->status, + 'enabled_events' => $this->enabled_events + ], fn (string|array|null $v): bool => $v !== null); + } +} diff --git a/src/Models/WebhookCreateInput.php b/src/Models/WebhookCreateInput.php new file mode 100644 index 0000000..dfcf013 --- /dev/null +++ b/src/Models/WebhookCreateInput.php @@ -0,0 +1,30 @@ + $this->url, + 'enabled_events' => $this->enabled_events + ], fn (string|array $v): true => $v !== null); + } +} diff --git a/src/Models/WebhookProperties.php b/src/Models/WebhookProperties.php new file mode 100644 index 0000000..ff22e5a --- /dev/null +++ b/src/Models/WebhookProperties.php @@ -0,0 +1,39 @@ + $this->organization, + 'livemode' => $this->livemode, + 'enabled_events' => $this->enabled_events, + 'url' => $this->url, + 'status' => $this->status + ], fn ($v): bool => $v !== null); + } +} diff --git a/src/Models/WebhookSearchResult.php b/src/Models/WebhookSearchResult.php new file mode 100644 index 0000000..19e817d --- /dev/null +++ b/src/Models/WebhookSearchResult.php @@ -0,0 +1,47 @@ + is_array($i) ? \Facturapi\Models\Webhook::fromArray($i) : $i, $data['data']) : [] + ); + } + + public function toArray(): array + { + return array_filter([ + 'page' => $this->page, + 'total_pages' => $this->total_pages, + 'total_results' => $this->total_results, + 'previous_cursor' => $this->previous_cursor, + 'next_cursor' => $this->next_cursor, + 'totals_are_capped' => $this->totals_are_capped, + 'data' => $this->data !== null ? array_map(fn (\Facturapi\Models\Webhook $i): array => $i->toArray(), $this->data) : null + ], fn (int|string|bool|array|null $v): bool => $v !== null); + } +} diff --git a/src/PaymentForm.php b/src/PaymentForm.php deleted file mode 100644 index 81c0c8b..0000000 --- a/src/PaymentForm.php +++ /dev/null @@ -1,26 +0,0 @@ -executeGetRequest( - $this->getRequestUrl("air-transport-codes", $params) - ) - ); - } catch (FacturapiException $e) { - throw $e; - } + return $this->client->get('catalogs/cartaporte/3.1/air-transport-codes', $params); } /** @@ -36,17 +32,9 @@ public function searchAirTransportCodes($params = null): mixed * @return mixed JSON-decoded response. * @throws FacturapiException */ - public function searchTransportConfigs($params = null): mixed + public function searchTransportConfigs(?array $params = null): mixed { - try { - return json_decode( - $this->executeGetRequest( - $this->getRequestUrl("transport-configs", $params) - ) - ); - } catch (FacturapiException $e) { - throw $e; - } + return $this->client->get('catalogs/cartaporte/3.1/transport-configs', $params); } /** @@ -56,17 +44,9 @@ public function searchTransportConfigs($params = null): mixed * @return mixed JSON-decoded response. * @throws FacturapiException */ - public function searchRightsOfPassage($params = null): mixed + public function searchRightsOfPassage(?array $params = null): mixed { - try { - return json_decode( - $this->executeGetRequest( - $this->getRequestUrl("rights-of-passage", $params) - ) - ); - } catch (FacturapiException $e) { - throw $e; - } + return $this->client->get('catalogs/cartaporte/3.1/rights-of-passage', $params); } /** @@ -76,17 +56,9 @@ public function searchRightsOfPassage($params = null): mixed * @return mixed JSON-decoded response. * @throws FacturapiException */ - public function searchCustomsDocuments($params = null): mixed + public function searchCustomsDocuments(?array $params = null): mixed { - try { - return json_decode( - $this->executeGetRequest( - $this->getRequestUrl("customs-documents", $params) - ) - ); - } catch (FacturapiException $e) { - throw $e; - } + return $this->client->get('catalogs/cartaporte/3.1/customs-documents', $params); } /** @@ -96,17 +68,9 @@ public function searchCustomsDocuments($params = null): mixed * @return mixed JSON-decoded response. * @throws FacturapiException */ - public function searchPackagingTypes($params = null): mixed + public function searchPackagingTypes(?array $params = null): mixed { - try { - return json_decode( - $this->executeGetRequest( - $this->getRequestUrl("packaging-types", $params) - ) - ); - } catch (FacturapiException $e) { - throw $e; - } + return $this->client->get('catalogs/cartaporte/3.1/packaging-types', $params); } /** @@ -116,17 +80,9 @@ public function searchPackagingTypes($params = null): mixed * @return mixed JSON-decoded response. * @throws FacturapiException */ - public function searchTrailerTypes($params = null): mixed + public function searchTrailerTypes(?array $params = null): mixed { - try { - return json_decode( - $this->executeGetRequest( - $this->getRequestUrl("trailer-types", $params) - ) - ); - } catch (FacturapiException $e) { - throw $e; - } + return $this->client->get('catalogs/cartaporte/3.1/trailer-types', $params); } /** @@ -136,17 +92,9 @@ public function searchTrailerTypes($params = null): mixed * @return mixed JSON-decoded response. * @throws FacturapiException */ - public function searchHazardousMaterials($params = null): mixed + public function searchHazardousMaterials(?array $params = null): mixed { - try { - return json_decode( - $this->executeGetRequest( - $this->getRequestUrl("hazardous-materials", $params) - ) - ); - } catch (FacturapiException $e) { - throw $e; - } + return $this->client->get('catalogs/cartaporte/3.1/hazardous-materials', $params); } /** @@ -156,17 +104,9 @@ public function searchHazardousMaterials($params = null): mixed * @return mixed JSON-decoded response. * @throws FacturapiException */ - public function searchNavalAuthorizations($params = null): mixed + public function searchNavalAuthorizations(?array $params = null): mixed { - try { - return json_decode( - $this->executeGetRequest( - $this->getRequestUrl("naval-authorizations", $params) - ) - ); - } catch (FacturapiException $e) { - throw $e; - } + return $this->client->get('catalogs/cartaporte/3.1/naval-authorizations', $params); } /** @@ -176,17 +116,9 @@ public function searchNavalAuthorizations($params = null): mixed * @return mixed JSON-decoded response. * @throws FacturapiException */ - public function searchPortStations($params = null): mixed + public function searchPortStations(?array $params = null): mixed { - try { - return json_decode( - $this->executeGetRequest( - $this->getRequestUrl("port-stations", $params) - ) - ); - } catch (FacturapiException $e) { - throw $e; - } + return $this->client->get('catalogs/cartaporte/3.1/port-stations', $params); } /** @@ -196,16 +128,8 @@ public function searchPortStations($params = null): mixed * @return mixed JSON-decoded response. * @throws FacturapiException */ - public function searchMarineContainers($params = null): mixed + public function searchMarineContainers(?array $params = null): mixed { - try { - return json_decode( - $this->executeGetRequest( - $this->getRequestUrl("marine-containers", $params) - ) - ); - } catch (FacturapiException $e) { - throw $e; - } + return $this->client->get('catalogs/cartaporte/3.1/marine-containers', $params); } } diff --git a/src/Resources/Catalogs.php b/src/Resources/Catalogs.php index b74eacb..b791e67 100644 --- a/src/Resources/Catalogs.php +++ b/src/Resources/Catalogs.php @@ -1,53 +1,41 @@ executeGetRequest( - $this->getRequestUrl("products", $params) - ) - ); - } catch (FacturapiException $e) { - throw $e; - } - } + /** + * Search a product key in SAT's catalog + * + * @param array|null $params Search parameters. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function searchProducts(?array $params = null): mixed + { + return $this->client->get('catalogs/products', $params); + } - /** - * Search a unit key in SAT's catalog - * - * @param array|null $params Search parameters. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function searchUnits($params = null): mixed - { - try { - return json_decode( - $this->executeGetRequest( - $this->getRequestUrl("units", $params) - ) - ); - } catch (FacturapiException $e) { - throw $e; - } - } + /** + * Search a unit key in SAT's catalog + * + * @param array|null $params Search parameters. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function searchUnits(?array $params = null): mixed + { + return $this->client->get('catalogs/units', $params); + } } diff --git a/src/Resources/ComercioExteriorCatalogs.php b/src/Resources/ComercioExteriorCatalogs.php index 2016eea..f6e9614 100644 --- a/src/Resources/ComercioExteriorCatalogs.php +++ b/src/Resources/ComercioExteriorCatalogs.php @@ -1,13 +1,17 @@ executeGetRequest( - $this->getRequestUrl("tariff-fractions", $params) - ) - ); - } catch (FacturapiException $e) { - throw $e; - } + return $this->client->get('catalogs/comercioexterior/2.0/tariff-fractions', $params); } } diff --git a/src/Resources/Customers.php b/src/Resources/Customers.php index c4ed04a..1a0ba59 100644 --- a/src/Resources/Customers.php +++ b/src/Resources/Customers.php @@ -1,111 +1,115 @@ executeGetRequest( $this->getRequestUrl( $params ) ) ); - } catch ( FacturapiException $e ) { - throw $e; - } - } + /** + * Get all Customers + * + * @param array|null $params Search parameters. + * + * @throws FacturapiException + */ + public function all(?array $params = null): CustomerSearchResult + { + $response = $this->client->get('customers', $params); + return CustomerSearchResult::fromArray($response); + } - /** - * Get a Customer by ID - * - * @param string $id Customer ID. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function retrieve( $id ): mixed { - try { - return json_decode( $this->executeGetRequest( $this->getRequestUrl( $id ) ) ); - } catch ( FacturapiException $e ) { - throw $e; - } - } + /** + * Get a Customer by ID + * + * @param string $id Customer ID. + * + * @throws FacturapiException + */ + public function retrieve(string $id): Customer + { + $response = $this->client->get('customers/' . $id); + return Customer::fromArray($response); + } - /** - * Create a Customer in your organization - * - * @param array $data Customer data. - * @param array|null $params Optional query parameters. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function create( $data, $params = null ): mixed { - try { - return json_decode( $this->executeJsonPostRequest( $this->getRequestUrl($params), $data ) ); - } catch ( FacturapiException $e ) { - throw $e; - } - } + /** + * Create a Customer in your organization + * + * @param array $data Customer data. + * @param array|null $params Optional query parameters. + * + * @throws FacturapiException + */ + public function create(array $data, ?array $params = null): Customer + { + $response = $this->client->post('customers', $data, $params); + return Customer::fromArray($response); + } - /** - * Update a Customer in your organization - * - * @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 ): mixed { - try { - return json_decode( $this->executeJsonPutRequest( $this->getRequestUrl( $id, $params ), $data ) ); - } catch ( FacturapiException $e ) { - throw $e; - } - } + /** + * Update a Customer in your organization + * + * @param string $id Customer ID. + * @param array $data Customer data. + * @param array|null $params Optional query parameters. + * + * @throws FacturapiException + */ + public function update(string $id, array $data, ?array $params = null): Customer + { + $response = $this->client->put('customers/' . $id, $data, $params); + return Customer::fromArray($response); + } - /** - * Delete a Customer in your organization - * - * @param string $id Customer ID. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function delete( $id ): mixed { - try { - return json_decode( $this->executeDeleteRequest( $this->getRequestUrl( $id ), null ) ); - } catch ( FacturapiException $e ) { - throw $e; - } - } + /** + * Delete a Customer in your organization + * + * @param string $id Customer ID. + * + * @throws FacturapiException + */ + public function delete(string $id): Customer + { + $response = $this->client->delete('customers/' . $id); + return Customer::fromArray($response); + } - /** - * Validates that a Customer's tax info is still valid - * - * @param string $id Customer ID. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function validateTaxInfo( $id ): mixed { - try { - return json_decode( $this->executeGetRequest( $this->getRequestUrl( $id ) . "/tax-info-validation" ) ); - } catch ( FacturapiException $e ) { - throw $e; - } - } + /** + * Validates that a Customer's tax info is still valid + * + * @param string $id Customer ID. + * + * @throws FacturapiException + */ + public function validateTaxInfo(string $id): TaxIdValidationResult + { + $response = $this->client->get('customers/' . $id . '/tax-info-validation'); + return TaxIdValidationResult::fromArray($response); + } + /** + * Send an edit link to the customer's email + * + * @param string $id Customer ID. + * @param array $data Data containing optional email. + * + * @throws FacturapiException + */ + public function sendEditLink(string $id, array $data = []): OkResponse + { + $response = $this->client->post('customers/' . $id . '/email-edit-link', $data); + return OkResponse::fromArray($response); + } } diff --git a/src/Resources/Invoices.php b/src/Resources/Invoices.php index a95eef0..d4e871d 100644 --- a/src/Resources/Invoices.php +++ b/src/Resources/Invoices.php @@ -1,414 +1,378 @@ executeGetRequest( $this->getRequestUrl( $query ) ) ); - } catch ( FacturapiException $e ) { - throw $e; - } - } - - /** - * Get an Invoice by ID - * - * @param string $id Invoice ID. - * @return mixed JSON-decoded response. - * @throws FacturapiException - */ - public function retrieve( $id ): mixed { - try { - return json_decode( $this->executeGetRequest( $this->getRequestUrl( $id ) ) ); - } catch ( FacturapiException $e ) { - throw $e; - } - } - - /** - * Gets the information needed to add this invoice as a related document in a - * payment complement (complemento de pago): the installment number according - * to the payment history, the previous balance, and the invoice tax breakdown - * prorated to the amount being paid. - * - * @param string $id Invoice ID. - * @param float $amount Amount being paid, expressed in the invoice currency. Cannot exceed the outstanding balance. - * @return mixed JSON-decoded related document summary. - * @throws FacturapiException - */ - public function paymentSummary( $id, $amount ): mixed { - try { - return json_decode( $this->executeGetRequest( $this->getRequestUrl( $id . '/payment-summary', array( 'amount' => $amount ) ) ) ); - } catch ( FacturapiException $e ) { - throw $e; - } - } - - /** - * Create an Invoice in your organization - * - * @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->executeJsonPostRequest( $this->getRequestUrl($query), $body) ); - } catch ( FacturapiException $e ) { - throw $e; - } - } - - - /** - * Cancel an Invoice in your organization - * - * @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->executeDeleteRequest( $this->getRequestUrl( $id, $query ), null ) ); - } catch ( FacturapiException $e ) { - throw $e; - } - } - - /** - * Sends the invoice to the customer's email - * - * @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->executeJsonPostRequest( - $this->getRequestUrl($id) . "/email", - array("email" => $email) - )); - } 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 string $id Invoice ID. - * @return string ZIP file contents. - * @throws FacturapiException - */ - public function downloadZip( $id ): string { - try { - 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 ); - } - - /** - * Creates or retrieves an invoice ZIP request - * - * @param array $body ZIP request payload. - * @return mixed JSON-decoded response. - * @throws FacturapiException - */ - public function createZipRequest( $body ): mixed { - try { - return json_decode( $this->executeJsonPostRequest( $this->getRequestUrl( "zip-requests" ), $body ) ); - } catch ( FacturapiException $e ) { - throw $e; - } - } - - /** - * Lists invoice ZIP requests - * - * @param array|null $query Query parameters. - * @return mixed JSON-decoded response. - * @throws FacturapiException - */ - public function listZipRequests( $query = null ): mixed { - try { - return json_decode( $this->executeGetRequest( $this->getRequestUrl( "zip-requests", $query ) ) ); - } catch ( FacturapiException $e ) { - throw $e; - } - } - - /** - * Retrieves an invoice ZIP request by ID - * - * @param string $id ZIP request ID. - * @return mixed JSON-decoded response. - * @throws FacturapiException - */ - public function retrieveZipRequest( $id ): mixed { - try { - return json_decode( $this->executeGetRequest( $this->getRequestUrl( "zip-requests/" . $id ) ) ); - } catch ( FacturapiException $e ) { - throw $e; - } - } - - /** - * Downloads a completed invoice ZIP request - * - * @param string $id ZIP request ID. - * @return string ZIP file contents. - * @throws FacturapiException - */ - public function downloadZipRequest( $id ): string { - try { - return $this->executeGetRequest( $this->getRequestUrl( "zip-requests/" . $id . "/zip" ) ); - } catch ( FacturapiException $e ) { - throw $e; - } - } - - /** - * Downloads the specified invoice in a PDF file - * - * @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->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 string $id Invoice ID. - * @return string XML file contents. - * @throws FacturapiException - */ - public function downloadXml( $id ): string { - try { - 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 string $id Invoice ID. - * @return string XML file contents. - * @throws FacturapiException - */ - public function downloadCancellationReceiptXml( $id ): string { - try { - 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 string $id Invoice ID. - * @return string PDF file contents. - * - * @throws FacturapiException - */ - public function downloadCancellationReceiptPdf( $id ): string { - try { - 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 string $id Invoice ID. - * @return mixed JSON-decoded response. - * @throws FacturapiException - */ - public function updateStatus( $id ): mixed { - try { - 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 string $id Invoice ID. - * @param array $body Draft payload. - * @return mixed JSON-decoded response. - * @throws FacturapiException - */ - public function updateDraft( $id, $body ): mixed { - try { - 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 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->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 string $id Invoice ID. - * @return mixed JSON-decoded response. - * @throws FacturapiException - */ - public function copyToDraft( $id ): mixed { - try { - 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 Invoice payload. - * @return string PDF file contents. - * @throws FacturapiException - */ - public function previewPdf( $body ): string { - try { - return $this->executeJsonPostRequest( $this->getRequestUrl( "preview/pdf" ), $body ); - } catch ( FacturapiException $e ) { - throw $e; - } - } - - /** - * @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 ); - } +use Facturapi\Http\FacturapiHttpClient; +use Facturapi\Models\Invoice; +use Facturapi\Models\InvoiceSearchResult; +use Facturapi\Models\InvoiceZipRequest; +use Facturapi\Models\InvoiceZipRequestSearchResult; +use Facturapi\Models\PaymentSummary; +use Facturapi\Models\SuccessResponse; + +class Invoices +{ + public function __construct(private readonly FacturapiHttpClient $client) + { + } + + + /** + * Get all Invoices + * + * @param array|null $query Query parameters. + * @return InvoiceSearchResult JSON-decoded response. + * @throws FacturapiException + */ + public function all(?array $query = null): InvoiceSearchResult + { + return InvoiceSearchResult::fromArray($this->client->get('invoices', $query)); + } + + /** + * Get an Invoice by ID + * + * @param string $id Invoice ID. + * @return Invoice JSON-decoded response. + * @throws FacturapiException + */ + public function retrieve(string $id): Invoice + { + return Invoice::fromArray($this->client->get('invoices/' . $id)); + } + + /** + * Gets the information needed to add this invoice as a related document in a + * payment complement (complemento de pago): the installment number according + * to the payment history, the previous balance, and the invoice tax breakdown + * prorated to the amount being paid. + * + * @param string $id Invoice ID. + * @param float $amount Amount being paid, expressed in the invoice currency. Cannot exceed the outstanding balance. + * @return PaymentSummary JSON-decoded related document summary. + * @throws FacturapiException + */ + public function paymentSummary(string $id, float $amount): PaymentSummary + { + return PaymentSummary::fromArray($this->client->get('invoices/' . $id . '/payment-summary', ['amount' => $amount])); + } + + /** + * Create an Invoice in your organization + * + * @param array $body Invoice payload. + * @param array|null $query Query parameters. + * @return Invoice JSON-decoded response. + * @throws FacturapiException + */ + public function create(array $body, ?array $query = null): Invoice + { + return Invoice::fromArray($this->client->post('invoices', $body, $query)); + } + + + /** + * Cancel an Invoice in your organization + * + * @param string $id Invoice ID. + * @param array|null $query URL query parameters. + * @return Invoice JSON-decoded response. + * @throws FacturapiException + */ + public function cancel(string $id, ?array $query = null): Invoice + { + return Invoice::fromArray($this->client->delete('invoices/' . $id, $query)); + } + + /** + * Sends the invoice to the customer's email + * + * @param string $id Invoice ID. + * @param string|array|null $email Email or list of emails. + * @return SuccessResponse JSON-decoded response. + * @throws FacturapiException + */ + public function sendByEmail(string $id, string|array|null $email = null): SuccessResponse + { + $payload = []; + if ($email !== null) { + $payload['email'] = is_array($email) ? $email : [$email]; + } + + return SuccessResponse::fromArray($this->client->post('invoices/' . $id . '/email', $payload)); + } + + /** + * @deprecated Use sendByEmail() instead. Will be removed in v5. + */ + public function send_by_email(string $id, string|array|null $email = null): SuccessResponse + { + 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 string $id Invoice ID. + * @return string ZIP file contents. + * @throws FacturapiException + */ + public function downloadZip($id): string + { + return $this->client->download('invoices/' . $id . '/zip'); + } + + /** + * @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); + } + + /** + * Creates or retrieves an invoice ZIP request + * + * @param array $body ZIP request payload. + * @return InvoiceZipRequest JSON-decoded response. + * @throws FacturapiException + */ + public function createZipRequest(array $body): InvoiceZipRequest + { + return InvoiceZipRequest::fromArray($this->client->post('invoices/zip-requests', $body)); + } + + /** + * Lists invoice ZIP requests + * + * @param array|null $query Query parameters. + * @return InvoiceZipRequestSearchResult JSON-decoded response. + * @throws FacturapiException + */ + public function listZipRequests(?array $query = null): InvoiceZipRequestSearchResult + { + return InvoiceZipRequestSearchResult::fromArray($this->client->get('invoices/zip-requests', $query)); + } + + /** + * Retrieves an invoice ZIP request by ID + * + * @param string $id ZIP request ID. + * @return InvoiceZipRequest JSON-decoded response. + * @throws FacturapiException + */ + public function retrieveZipRequest(string $id): InvoiceZipRequest + { + return InvoiceZipRequest::fromArray($this->client->get('invoices/zip-requests/' . $id)); + } + + /** + * Downloads a completed invoice ZIP request + * + * @param string $id ZIP request ID. + * @return string ZIP file contents. + * @throws FacturapiException + */ + public function downloadZipRequest($id): string + { + return $this->client->download('invoices/zip-requests/' . $id . '/zip'); + } + + /** + * Downloads the specified invoice in a PDF file + * + * @param string $id Invoice ID. + * @return string Raw PDF bytes (binary string, not base64-encoded). + * @throws FacturapiException + */ + public function downloadPdf($id): string + { + return $this->client->download('invoices/' . $id . '/pdf'); + } + + /** + * @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 string $id Invoice ID. + * @return string XML file contents. + * @throws FacturapiException + */ + public function downloadXml($id): string + { + return $this->client->download('invoices/' . $id . '/xml'); + } + + /** + * @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 string $id Invoice ID. + * @return string XML file contents. + * @throws FacturapiException + */ + public function downloadCancellationReceiptXml($id): string + { + return $this->client->download('invoices/' . $id . '/cancellation_receipt/xml'); + } + + /** + * @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 string $id Invoice ID. + * @return string PDF file contents. + * + * @throws FacturapiException + */ + public function downloadCancellationReceiptPdf($id): string + { + return $this->client->download('invoices/' . $id . '/cancellation_receipt/pdf'); + } + + /** + * @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 string $id Invoice ID. + * @return Invoice JSON-decoded response. + * @throws FacturapiException + */ + public function updateStatus(string $id): Invoice + { + return Invoice::fromArray($this->client->put('invoices/' . $id . '/status', [])); + } + + /** + * @deprecated Use updateStatus() instead. Will be removed in v5. + */ + public function update_status(string $id): Invoice + { + 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 string $id Invoice ID. + * @param array $body Draft payload. + * @return Invoice JSON-decoded response. + * @throws FacturapiException + */ + public function updateDraft(string $id, array $body): Invoice + { + return Invoice::fromArray($this->client->put('invoices/' . $id, $body)); + } + + /** + * @deprecated Use updateDraft() instead. Will be removed in v5. + */ + public function update_draft(string $id, array $body): Invoice + { + 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 string $id Invoice ID. + * @param array|null $query URL query parameters. + * @return Invoice JSON-decoded response. + * @throws FacturapiException + */ + public function stampDraft(string $id, ?array $query = null): Invoice + { + return Invoice::fromArray($this->client->post('invoices/' . $id . '/stamp', [], $query)); + } + + /** + * @deprecated Use stampDraft() instead. Will be removed in v5. + */ + public function stamp_draft(string $id, ?array $query = null): Invoice + { + 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 string $id Invoice ID. + * @return Invoice JSON-decoded response. + * @throws FacturapiException + */ + public function copyToDraft(string $id): Invoice + { + return Invoice::fromArray($this->client->post('invoices/' . $id . '/copy', [])); + } + + /** + * @deprecated Use copyToDraft() instead. Will be removed in v5. + */ + public function copy_to_draft(string $id): Invoice + { + 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 Invoice payload. + * @return string PDF file contents. + * @throws FacturapiException + */ + public function previewPdf(array $body): string + { + return $this->client->postRaw('invoices/preview/pdf', $body); + } + + /** + * @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/NominaCatalogs.php b/src/Resources/NominaCatalogs.php index a94ed07..8aca46d 100644 --- a/src/Resources/NominaCatalogs.php +++ b/src/Resources/NominaCatalogs.php @@ -1,28 +1,29 @@ search('deductions', $params); } - public function searchPerceptions($params = null): mixed + public function searchPerceptions(?array $params = null): mixed { return $this->search('perceptions', $params); } - private function search(string $path, $params): mixed + private function search(string $path, ?array $params): mixed { - try { - return json_decode($this->executeGetRequest($this->getRequestUrl($path, $params))); - } catch (FacturapiException $e) { - throw $e; - } + return $this->client->get('catalogs/nomina/' . $path, $params); } } diff --git a/src/Resources/Organizations.php b/src/Resources/Organizations.php index f2d2307..20bf0aa 100644 --- a/src/Resources/Organizations.php +++ b/src/Resources/Organizations.php @@ -1,814 +1,612 @@ executeGetRequest($this->getRequestUrl($params))); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Get an Organization by ID. - * - * @param string $id Organization ID. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function retrieve($id): mixed - { - try { - return json_decode($this->executeGetRequest($this->getRequestUrl($id))); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Create an Organization. - * - * @param array $params Organization payload. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function create($params): mixed - { - try { - return json_decode($this->executeJsonPostRequest($this->getRequestUrl(), $params)); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Update an organization's legal information. - * - * @param string $id Organization ID. - * @param array $params Legal information payload. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function updateLegal($id, $params): mixed - { - try { - return json_decode($this->executeJsonPutRequest($this->getRequestUrl($id) . "/legal", $params)); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Update an organization's customization information. - * - * @param string $id Organization ID. - * @param array $params Customization payload. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function updateCustomization($id, $params): mixed - { - try { - return json_decode($this->executeJsonPutRequest($this->getRequestUrl($id) . "/customization", $params)); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Update an organization's receipt settings. - * - * @param string $id Organization ID. - * @param array $params Receipt settings payload. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function updateReceiptSettings($id, $params): mixed - { - try { - return json_decode($this->executeJsonPutRequest($this->getRequestUrl($id) . "/receipts", $params)); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Update an organization's domain. - * - * @param string $id Organization ID. - * @param array $params Domain payload. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function updateDomain($id, $params): mixed - { - try { - return json_decode($this->executeJsonPutRequest($this->getRequestUrl($id) . "/domain", $params)); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Check domain availability. - * - * @param array $query Domain check query parameters. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function checkDomainAvailability($query): mixed - { - if (!is_array($query)) { - throw new FacturapiException('checkDomainAvailability expects $query to be an array.'); - } - - try { - return json_decode( - $this->executeGetRequest( - $this->getRequestUrl("domain-check", $query) - ) - ); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * @deprecated Use checkDomainAvailability($query) instead. This alias will be removed in v5. - * - * @param array|string $idOrQuery Domain check query parameters, or legacy organization ID. - * @param array|null $query Legacy query parameters when using historical signature. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function checkDomainIsAvailable($idOrQuery, $query = null): mixed - { - trigger_error( - 'Organizations::checkDomainIsAvailable(...) is deprecated and will be removed in v5. Use checkDomainAvailability($query) instead.', - E_USER_DEPRECATED - ); - - $argsCount = func_num_args(); - if ($argsCount === 1 && is_array($idOrQuery)) { - return $this->checkDomainAvailability($idOrQuery); - } - - if ($argsCount >= 2 && is_array($query)) { - // Backward compatibility with historical signature: ($id, $params). - return $this->checkDomainAvailability($query); - } - - throw new FacturapiException('checkDomainIsAvailable expects either ($query) or ($id, $params).'); - } - - /** - * Uploads the organization's logo - * - * @param string $id Organization ID. - * @param string $params Logo file path. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function uploadLogo($id, $params): mixed - { - try { - return json_decode($this->executeDataPutRequest($this->getRequestUrl($id) . "/logo", $params)); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Uploads the organization's certificate (CSD) - * - * @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): mixed - { - try { - return json_decode($this->executeDataPutRequest($this->getRequestUrl($id) . "/certificate", $params)); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Get the test API key for an organization. - * - * @param string $id Organization ID. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function getTestApiKey($id): mixed - { - try { - return json_decode($this->executeGetRequest($this->getRequestUrl($id) . "/apikeys/test")); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Renew the test API key for an organization. - * - * @param string $id Organization ID. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function renewTestApiKey($id): mixed - { - try { - return json_decode( - $this->executeJsonPutRequest( - $this->getRequestUrl($id) . "/apikeys/test", - [] - ) - ); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * @deprecated Use listLiveApiKeys() instead. Will be removed in v5. - * - * @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. - * - * @param string $id Organization ID. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function listLiveApiKeys($id): mixed - { - try { - return json_decode( - $this->executeGetRequest( - $this->getRequestUrl($id) . "/apikeys/live" - ) - ); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Renew the live API key for an organization. - * - * @param string $id Organization ID. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function renewLiveApiKey($id): mixed - { - try { - return json_decode( - $this->executeJsonPutRequest( - $this->getRequestUrl($id) . "/apikeys/live", - [] - ) - ); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Delete one live API key for an organization. - * - * @param string $organizationId Organization ID. - * @param string $apiKeyId API key ID. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function deleteLiveApiKey($organizationId, $apiKeyId): mixed - { - try { - return json_decode( - $this->executeJsonPutRequest( - $this->getRequestUrl($organizationId) . "/apikeys/live" . "/" . $apiKeyId, - [] - ) - ); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Delete an organization. - * - * @param string $id Organization ID. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function delete($id): mixed - { - try { - return json_decode($this->executeDeleteRequest($this->getRequestUrl($id), null)); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Delete an organization's certificate. - * - * @param string $id Organization ID. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function deleteCertificate($id): mixed - { - try { - return json_decode($this->executeDeleteRequest($this->getRequestUrl($id) . "/certificate", null)); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Get series groups for an organization. - * - * @param string $id Organization ID. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function getSeriesGroup($id): mixed - { - try { - return json_decode($this->executeGetRequest($this->getRequestUrl($id) . "/series-group")); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Create a series group for an organization. - * - * @param string $id Organization ID. - * @param array $params Series group payload. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function createSeriesGroup($id, $params): mixed - { - try { - return json_decode($this->executeJsonPostRequest($this->getRequestUrl($id) . "/series-group", $params)); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Update a series group for an organization. - * - * @param string $id Organization ID. - * @param string $series_name Series name. - * @param array $params Series group payload. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function updateSeriesGroup($id, $series_name, $params): mixed - { - try { - return json_decode($this->executeJsonPutRequest($this->getRequestUrl($id) . "/series-group" . "/" . $series_name, $params)); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Update default series for an organization. - * - * @param string $id Organization ID. - * @param array $params Default series payload. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function updateDefaultSeries($id, $params): mixed - { - try { - return json_decode( - $this->executeJsonPutRequest( - $this->getRequestUrl($id) . "/series-group/default-series", - $params - ) - ); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Delete a series group for an organization. - * - * @param string $id Organization ID. - * @param string $series_name Series name. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function deleteSeriesGroup($id, $series_name): mixed - { - try { - return json_decode($this->executeDeleteRequest($this->getRequestUrl($id) . "/series-group" . "/" . $series_name, null)); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Update self-invoice settings for an Organization - * - * @param string $id Organization ID. - * @param array $params Self-invoice settings payload. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function updateSelfInvoiceSettings($id, $params): mixed - { - try { - return json_decode($this->executeJsonPutRequest($this->getRequestUrl($id) . "/self-invoice", $params)); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * List users with access to an organization. - * - * @param string $organizationId Organization ID. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function listTeamAccess($organizationId): mixed - { - try { - return json_decode($this->executeGetRequest($this->getRequestUrl($organizationId) . "/team")); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Retrieve one user access entry within an organization. - * - * @param string $organizationId Organization ID. - * @param string $accessId Access ID. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function retrieveTeamAccess($organizationId, $accessId): mixed - { - try { - return json_decode($this->executeGetRequest($this->getRequestUrl($organizationId) . "/team/" . $accessId)); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Reassign a role to a user access entry. - * - * @param string $organizationId Organization ID. - * @param string $accessId Access ID. - * @param string $role Role ID. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function updateTeamAccessRole($organizationId, $accessId, $role): mixed - { - try { - return json_decode( - $this->executeJsonPutRequest( - $this->getRequestUrl($organizationId) . "/team/" . $accessId . "/role", - ["role" => $role] - ) - ); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Remove a user access entry from an organization. - * - * @param string $organizationId Organization ID. - * @param string $accessId Access ID. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function removeTeamAccess($organizationId, $accessId): mixed - { - try { - return json_decode( - $this->executeDeleteRequest( - $this->getRequestUrl($organizationId) . "/team/" . $accessId, - null - ) - ); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * List sent team invites for an organization. - * - * @param string $organizationId Organization ID. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function listSentTeamInvites($organizationId): mixed - { - try { - return json_decode($this->executeGetRequest($this->getRequestUrl($organizationId) . "/team/invites")); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Create or update a team invite for an organization. - * - * @param string $organizationId Organization ID. - * @param array $params Invite payload. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function inviteUserToTeam($organizationId, $params): mixed - { - try { - return json_decode( - $this->executeJsonPostRequest( - $this->getRequestUrl($organizationId) . "/team/invites", - $params - ) - ); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Cancel a pending team invite. - * - * @param string $organizationId Organization ID. - * @param string $inviteKey Invite key. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function cancelTeamInvite($organizationId, $inviteKey): mixed - { - try { - return json_decode( - $this->executeDeleteRequest( - $this->getRequestUrl($organizationId) . "/team/invites/" . $inviteKey, - null - ) - ); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * List pending invites received by the authenticated user. - * - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function listReceivedTeamInvites(): mixed - { - try { - return json_decode($this->executeGetRequest($this->getRequestUrl("invites/pending"))); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Respond to a team invite. - * - * @param string $inviteKey Invite key. - * @param array $params Response payload. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function respondTeamInvite($inviteKey, $params): mixed - { - try { - return json_decode( - $this->executeJsonPostRequest( - $this->getRequestUrl("invites/" . $inviteKey . "/response"), - $params - ) - ); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * List organization team roles. - * - * @param string $organizationId Organization ID. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function listTeamRoles($organizationId): mixed - { - try { - return json_decode($this->executeGetRequest($this->getRequestUrl($organizationId) . "/team/roles")); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * List team role templates. - * - * @param string $organizationId Organization ID. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function listTeamRoleTemplates($organizationId): mixed - { - try { - return json_decode($this->executeGetRequest($this->getRequestUrl($organizationId) . "/team/roles/templates")); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * List team role operations. - * - * @param string $organizationId Organization ID. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function listTeamRoleOperations($organizationId): mixed - { - try { - return json_decode($this->executeGetRequest($this->getRequestUrl($organizationId) . "/team/roles/operations")); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Retrieve a team role by ID. - * - * @param string $organizationId Organization ID. - * @param string $roleId Role ID. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function retrieveTeamRole($organizationId, $roleId): mixed - { - try { - return json_decode($this->executeGetRequest($this->getRequestUrl($organizationId) . "/team/roles/" . $roleId)); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Create a team role. - * - * @param string $organizationId Organization ID. - * @param array $params Role payload. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function createTeamRole($organizationId, $params): mixed - { - try { - return json_decode( - $this->executeJsonPostRequest( - $this->getRequestUrl($organizationId) . "/team/roles", - $params - ) - ); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Update a team role. - * - * @param string $organizationId Organization ID. - * @param string $roleId Role ID. - * @param array $params Role payload. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function updateTeamRole($organizationId, $roleId, $params): mixed - { - try { - return json_decode( - $this->executeJsonPutRequest( - $this->getRequestUrl($organizationId) . "/team/roles/" . $roleId, - $params - ) - ); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Delete a team role. - * - * @param string $organizationId Organization ID. - * @param string $roleId Role ID. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function deleteTeamRole($organizationId, $roleId): mixed - { - try { - return json_decode( - $this->executeDeleteRequest( - $this->getRequestUrl($organizationId) . "/team/roles/" . $roleId, - null - ) - ); - } catch (FacturapiException $e) { - throw $e; - } - } + public function __construct(private readonly FacturapiHttpClient $client) + { + } + + + /** + * Get all Organizations + * + * @param array|null $params Search parameters. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function all(?array $params = null): OrganizationSearchResult + { + return OrganizationSearchResult::fromArray($this->client->get('organizations', $params)); + } + + /** + * Get an Organization by ID. + * + * @param string $id Organization ID. + * @return Organization JSON-decoded response. + * + * @throws FacturapiException + */ + public function retrieve(string $id): Organization + { + return Organization::fromArray($this->client->get('organizations/' . $id)); + } + + /** + * Create an Organization. + * + * @param array $params Organization payload. + * @return Organization JSON-decoded response. + * + * @throws FacturapiException + */ + public function create(array $params): Organization + { + return Organization::fromArray($this->client->post('organizations', $params)); + } + + /** + * Update an organization's legal information. + * + * @param string $id Organization ID. + * @param array $params Legal information payload. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function updateLegal(string $id, array $params): Organization + { + return Organization::fromArray($this->client->put('organizations/' . $id . '/legal', $params)); + } + + /** + * Update an organization's customization information. + * + * @param string $id Organization ID. + * @param array $params Customization payload. + * @return Organization JSON-decoded response. + * + * @throws FacturapiException + */ + public function updateCustomization(string $id, array $params): Organization + { + return Organization::fromArray($this->client->put('organizations/' . $id . '/customization', $params)); + } + + /** + * Update an organization's receipt settings. + * + * @param string $id Organization ID. + * @param array $params Receipt settings payload. + * @return Organization JSON-decoded response. + * + * @throws FacturapiException + */ + public function updateReceiptSettings(string $id, array $params): Organization + { + return Organization::fromArray($this->client->put('organizations/' . $id . '/receipts', $params)); + } + + /** + * Update an organization's domain. + * + * @param string $id Organization ID. + * @param array $params Domain payload. + * @return Organization JSON-decoded response. + * + * @throws FacturapiException + */ + public function updateDomain(string $id, array $params): Organization + { + return Organization::fromArray($this->client->put('organizations/' . $id . '/domain', $params)); + } + + /** + * Check domain availability. + * + * @param array $query Domain check query parameters. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function checkDomainAvailability($query): DomainAvailability + { + if (!is_array($query)) { + throw new FacturapiException('checkDomainAvailability expects $query to be an array.'); + } + + return DomainAvailability::fromArray($this->client->get('organizations/' . 'domain-check', $query)); + } + + /** + * @deprecated Use checkDomainAvailability($query) instead. This alias will be removed in v5. + * + * @param array|string $idOrQuery Domain check query parameters, or legacy organization ID. + * @param array|null $query Legacy query parameters when using historical signature. + * @return DomainAvailability JSON-decoded response. + * + * @throws FacturapiException + */ + public function checkDomainIsAvailable($idOrQuery, ?array $query = null): DomainAvailability + { + trigger_error( + 'Organizations::checkDomainIsAvailable(...) is deprecated and will be removed in v5. Use checkDomainAvailability($query) instead.', + E_USER_DEPRECATED + ); + + $argsCount = func_num_args(); + if ($argsCount === 1 && is_array($idOrQuery)) { + return $this->checkDomainAvailability($idOrQuery); + } + + if ($argsCount >= 2 && is_array($query)) { + // Backward compatibility with historical signature: ($id, $params). + return $this->checkDomainAvailability($query); + } + + throw new FacturapiException('checkDomainIsAvailable expects either ($query) or ($id, $params).'); + } + + /** + * Uploads the organization's logo + * + * @param string $id Organization ID. + * @param string $params Logo file path. + * @return Organization JSON-decoded response. + * + * @throws FacturapiException + */ + public function uploadLogo(string $id, string $file): Organization + { + return Organization::fromArray($this->client->multipartRequest('PUT', 'organizations/' . $id . '/logo', ['file' => $file])); + } + + /** + * Uploads the organization's certificate (CSD) + * + * @param string $id Organization ID. + * @param array $params Certificate payload (`cerFile`, `keyFile`, `password`). + * @return Organization JSON-decoded response. + * + * @throws FacturapiException + */ + public function uploadCertificate(string $id, array $params): Organization + { + $files = []; + $data = []; + if (!isset($params['cerFile']) || !isset($params['keyFile']) || !isset($params['password'])) { + throw new \Facturapi\Exceptions\FacturapiException('Invalid certificate payload. Expected cerFile, keyFile and password.'); + } + if (isset($params['cerFile'])) { + $files['cer'] = $params['cerFile']; + } + if (isset($params['keyFile'])) { + $files['key'] = $params['keyFile']; + } + if (isset($params['password'])) { + $data['password'] = $params['password']; + } + + return Organization::fromArray($this->client->multipartRequest('PUT', 'organizations/' . $id . '/certificate', $files, $data)); + } + + /** + * Get the test API key for an organization. + * + * @param string $id Organization ID. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function getTestApiKey($id): mixed + { + return $this->client->get('organizations/' . $id . '/apikeys/test'); + } + + /** + * Renew the test API key for an organization. + * + * @param string $id Organization ID. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function renewTestApiKey($id): mixed + { + return $this->client->put('organizations/' . $id . '/apikeys/test', []); + } + + /** + * @deprecated Use listLiveApiKeys() instead. Will be removed in v5. + * + * @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. + * + * @param string $id Organization ID. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function listLiveApiKeys($id): mixed + { + return $this->client->get('organizations/' . $id . '/apikeys/live'); + } + + /** + * Renew the live API key for an organization. + * + * @param string $id Organization ID. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function renewLiveApiKey($id): mixed + { + return $this->client->put('organizations/' . $id . '/apikeys/live', []); + } + + /** + * Delete one live API key for an organization. + * + * @param string $organizationId Organization ID. + * @param string $apiKeyId API key ID. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function deleteLiveApiKey($organizationId, $apiKeyId): mixed + { + return $this->client->delete('organizations/' . $organizationId . '/apikeys/live/' . $apiKeyId); + } + + /** + * Delete an organization. + * + * @param string $id Organization ID. + * @return Organization JSON-decoded response. + * + * @throws FacturapiException + */ + public function delete(string $id): Organization + { + return Organization::fromArray($this->client->delete('organizations/' . $id)); + } + + /** + * Delete an organization's certificate. + * + * @param string $id Organization ID. + * @return Organization JSON-decoded response. + * + * @throws FacturapiException + */ + public function deleteCertificate(string $id): Organization + { + return Organization::fromArray($this->client->delete('organizations/' . $id . '/certificate')); + } + + /** + * Get series groups for an organization. + * + * @param string $id Organization ID. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function getSeriesGroup($id): mixed + { + return $this->client->get('organizations/' . $id . '/series-group'); + } + + /** + * Create a series group for an organization. + * + * @param string $id Organization ID. + * @param array $params Series group payload. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function createSeriesGroup($id, array $params): mixed + { + return $this->client->post('organizations/' . $id . '/series-group', $params); + } + + /** + * Update a series group for an organization. + * + * @param string $id Organization ID. + * @param string $series_name Series name. + * @param array $params Series group payload. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function updateSeriesGroup(string $id, string $series_name, array $params): mixed + { + return $this->client->put('organizations/' . $id . '/series-group/' . $series_name, $params); + } + + /** + * Update default series for an organization. + * + * @param string $id Organization ID. + * @param array $params Default series payload. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function updateDefaultSeries($id, array $params): mixed + { + return $this->client->put('organizations/' . $id . '/series-group/default-series', $params); + } + + /** + * Delete a series group for an organization. + * + * @param string $id Organization ID. + * @param string $series_name Series name. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function deleteSeriesGroup(string $id, string $series_name): mixed + { + return $this->client->delete('organizations/' . $id . '/series-group/' . $series_name); + } + + /** + * Update self-invoice settings for an Organization + * + * @param string $id Organization ID. + * @param array $params Self-invoice settings payload. + * @return Organization JSON-decoded response. + * + * @throws FacturapiException + */ + public function updateSelfInvoiceSettings(string $id, array $params): Organization + { + return Organization::fromArray($this->client->put('organizations/' . $id . '/self-invoice', $params)); + } + + /** + * List users with access to an organization. + * + * @param string $organizationId Organization ID. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function listTeamAccess($organizationId): mixed + { + return $this->client->get('organizations/' . $organizationId . '/team'); + } + + /** + * Retrieve one user access entry within an organization. + * + * @param string $organizationId Organization ID. + * @param string $accessId Access ID. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function retrieveTeamAccess($organizationId, $accessId): mixed + { + return $this->client->get('organizations/' . $organizationId . '/team/' . $accessId); + } + + /** + * Reassign a role to a user access entry. + * + * @param string $organizationId Organization ID. + * @param string $accessId Access ID. + * @param string $role Role ID. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function updateTeamAccessRole($organizationId, $accessId, $role): mixed + { + return $this->client->put('organizations/' . $organizationId . '/team/' . $accessId . '/role', ['role' => $role]); + } + + /** + * Remove a user access entry from an organization. + * + * @param string $organizationId Organization ID. + * @param string $accessId Access ID. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function removeTeamAccess($organizationId, $accessId): mixed + { + return $this->client->delete('organizations/' . $organizationId . '/team/' . $accessId); + } + + /** + * List sent team invites for an organization. + * + * @param string $organizationId Organization ID. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function listSentTeamInvites($organizationId): mixed + { + return $this->client->get('organizations/' . $organizationId . '/team/invites'); + } + + /** + * Create or update a team invite for an organization. + * + * @param string $organizationId Organization ID. + * @param array $params Invite payload. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function inviteUserToTeam($organizationId, array $params): mixed + { + return $this->client->post('organizations/' . $organizationId . '/team/invites', $params); + } + + /** + * Cancel a pending team invite. + * + * @param string $organizationId Organization ID. + * @param string $inviteKey Invite key. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function cancelTeamInvite($organizationId, $inviteKey): mixed + { + return $this->client->delete('organizations/' . $organizationId . '/team/invites/' . $inviteKey); + } + + /** + * List pending invites received by the authenticated user. + * + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function listReceivedTeamInvites(): mixed + { + return $this->client->get('organizations/' . 'invites/pending'); + } + + /** + * Respond to a team invite. + * + * @param string $inviteKey Invite key. + * @param array $params Response payload. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function respondTeamInvite($inviteKey, array $params): mixed + { + return $this->client->post('organizations/' . 'invites/' . $inviteKey . '/response', $params); + } + + /** + * List organization team roles. + * + * @param string $organizationId Organization ID. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function listTeamRoles($organizationId): mixed + { + return $this->client->get('organizations/' . $organizationId . '/team/roles'); + } + + /** + * List team role templates. + * + * @param string $organizationId Organization ID. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function listTeamRoleTemplates($organizationId): mixed + { + return $this->client->get('organizations/' . $organizationId . '/team/roles/templates'); + } + + /** + * List team role operations. + * + * @param string $organizationId Organization ID. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function listTeamRoleOperations($organizationId): mixed + { + return $this->client->get('organizations/' . $organizationId . '/team/roles/operations'); + } + + /** + * Retrieve a team role by ID. + * + * @param string $organizationId Organization ID. + * @param string $roleId Role ID. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function retrieveTeamRole($organizationId, $roleId): mixed + { + return $this->client->get('organizations/' . $organizationId . '/team/roles/' . $roleId); + } + + /** + * Create a team role. + * + * @param string $organizationId Organization ID. + * @param array $params Role payload. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function createTeamRole($organizationId, array $params): mixed + { + return $this->client->post('organizations/' . $organizationId . '/team/roles', $params); + } + + /** + * Update a team role. + * + * @param string $organizationId Organization ID. + * @param string $roleId Role ID. + * @param array $params Role payload. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function updateTeamRole($organizationId, $roleId, array $params): mixed + { + return $this->client->put('organizations/' . $organizationId . '/team/roles/' . $roleId, $params); + } + + /** + * Delete a team role. + * + * @param string $organizationId Organization ID. + * @param string $roleId Role ID. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function deleteTeamRole($organizationId, $roleId): mixed + { + return $this->client->delete('organizations/' . $organizationId . '/team/roles/' . $roleId); + } } diff --git a/src/Resources/Products.php b/src/Resources/Products.php index bd1c588..649b9b8 100644 --- a/src/Resources/Products.php +++ b/src/Resources/Products.php @@ -1,94 +1,86 @@ executeGetRequest( $this->getRequestUrl( $params ) ) ); - } catch ( FacturapiException $e ) { - throw $e; - } - } + /** + * Get all Products + * + * @param array|null $params Search parameters. + * + * @throws FacturapiException + */ + public function all(?array $params = null): ProductSearchResult + { + $response = $this->client->get('products', $params); + return ProductSearchResult::fromArray($response); + } - /** - * Get a Product by ID - * - * @param string $id Product ID. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function retrieve( $id ): mixed { - try { - return json_decode( $this->executeGetRequest( $this->getRequestUrl( $id ) ) ); - } catch ( FacturapiException $e ) { - throw $e; - } - } + /** + * Get a Product by ID + * + * @param string $id Product ID. + * + * @throws FacturapiException + */ + public function retrieve(string $id): Product + { + $response = $this->client->get('products/' . $id); + return Product::fromArray($response); + } - /** - * Create a Product in your organization - * - * @param array $params Product data. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function create( $params ): mixed { - try { - return json_decode( $this->executeJsonPostRequest( $this->getRequestUrl(), $params ) ); - } catch ( FacturapiException $e ) { - throw $e; - } - } + /** + * Create a Product in your organization + * + * @param array $params Product data. + * + * @throws FacturapiException + */ + public function create(array $params): Product + { + $response = $this->client->post('products', $params); + return Product::fromArray($response); + } - /** - * Update a Product in your organization - * - * @param string $id Product ID. - * @param array $params Product data. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function update( $id, $params ): mixed { - try { - return json_decode( $this->executeJsonPutRequest( $this->getRequestUrl( $id ), $params ) ); - } catch ( FacturapiException $e ) { - throw $e; - } - } + /** + * Update a Product in your organization + * + * @param string $id Product ID. + * @param array $params Product data. + * + * @throws FacturapiException + */ + public function update(string $id, array $params): Product + { + $response = $this->client->put('products/' . $id, $params); + return Product::fromArray($response); + } - /** - * Delete a Product in your organization - * - * @param string $id Product ID. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function delete( $id ): mixed { - try { - return json_decode( $this->executeDeleteRequest( $this->getRequestUrl( $id ), null ) ); - } catch ( FacturapiException $e ) { - throw $e; - } - } + /** + * Delete a Product in your organization + * + * @param string $id Product ID. + * + * @throws FacturapiException + */ + public function delete(string $id): Product + { + $response = $this->client->delete('products/' . $id); + return Product::fromArray($response); + } } diff --git a/src/Resources/Receipts.php b/src/Resources/Receipts.php index 626b554..fb705eb 100644 --- a/src/Resources/Receipts.php +++ b/src/Resources/Receipts.php @@ -1,208 +1,195 @@ executeGetRequest($this->getRequestUrl($params))); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Get a Receipt by ID - * - * @param string $id Receipt ID. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function retrieve($id): mixed - { - try { - return json_decode($this->executeGetRequest($this->getRequestUrl($id))); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Create a Receipt in your organization - * - * @param array $params Receipt payload. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function create($params): mixed - { - try { - return json_decode($this->executeJsonPostRequest($this->getRequestUrl(), $params)); - } catch (FacturapiException $e) { - throw $e; - } - } - - - /** - * Creates an invoice for a Receipt - * - * @param string $id Receipt ID. - * @param array $params Invoice payload. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function invoice($id, $params): mixed - { - try { - 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 array $params Global invoice payload. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function createGlobalInvoice($params): mixed - { - try { - return json_decode($this->executeJsonPostRequest($this->getRequestUrl() . "/global-invoice", $params)); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Creates a single invoice using a list of receipt keys - * - * @param $data Array of properties and property values for creating an invoice from multiple receipts - * - * @return Response The created invoice object or preview summary when dry_run is enabled - * - * @throws FacturapiException - */ - public function toInvoice($data) - { - try { - return json_decode($this->executeJsonPostRequest($this->getRequestUrl() . "/to-invoice", $data)); - } catch (FacturapiException $e) { - throw new FacturapiException('Unable to create invoice: ' . $e->getMessage()); - } - } - - /** - * Generates an invoice PDF preview for multiple receipts without stamping or saving it - * - * @param $data Array of properties and property values for generating an invoice PDF preview - * - * @return string PDF file contents - * - * @throws Facturapi_Exception - */ - public function previewToInvoicePdf($data) - { - try { - return $this->executeJsonPostRequest($this->getRequestUrl() . "/to-invoice/preview", $data); - } catch (FacturapiException $e) { - throw new FacturapiException('Unable to generate invoice PDF preview: ' . $e->getMessage()); - } - } - - /** - * Cancel a Receipt - * - * @param string $id Receipt ID. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function cancel($id): mixed - { - try { - return json_decode($this->executeDeleteRequest($this->getRequestUrl($id), null)); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Sends the receipt to the customer's email - * - * @param string $id Receipt 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->executeJsonPostRequest( - $this->getRequestUrl($id) . "/email", - $email == null ? null : array("email" => $email) - )); - } 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 string $id Receipt ID. - * @return string Raw PDF bytes (binary string, not base64-encoded). - * - * @throws FacturapiException - */ - public function downloadPdf($id): string - { - try { - 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('Receipts::download_pdf() is deprecated and will be removed in v5. Use downloadPdf() instead.', E_USER_DEPRECATED); - return $this->downloadPdf($id); - } + public function __construct(private readonly FacturapiHttpClient $client) + { + } + + + /** + * Search or list all receipts in your organization + * + * @param array|null $params Search parameters. + * @return ReceiptSearchResult JSON-decoded response. + * + * @throws FacturapiException + */ + public function all(?array $params = null): ReceiptSearchResult + { + return ReceiptSearchResult::fromArray($this->client->get('receipts', $params)); + } + + /** + * Get a Receipt by ID + * + * @param string $id Receipt ID. + * @return Receipt JSON-decoded response. + * + * @throws FacturapiException + */ + public function retrieve(string $id): Receipt + { + return Receipt::fromArray($this->client->get('receipts/' . $id)); + } + + /** + * Create a Receipt in your organization + * + * @param array $params Receipt payload. + * @return Receipt JSON-decoded response. + * + * @throws FacturapiException + */ + public function create(array $params): Receipt + { + return Receipt::fromArray($this->client->post('receipts', $params)); + } + + + /** + * Creates an invoice for a Receipt + * + * @param string $id Receipt ID. + * @param array $params Invoice payload. + * @return Invoice JSON-decoded response. + * + * @throws FacturapiException + */ + public function invoice(string $id, array $params): Invoice + { + return Invoice::fromArray($this->client->post('receipts/' . $id . '/invoice', $params)); + } + + /** + * Creates a global invoice from the open receipts in the last completed period + * + * @param array $params Global invoice payload. + * @return Invoice JSON-decoded response. + * + * @throws FacturapiException + */ + public function createGlobalInvoice(array $params): Invoice + { + return Invoice::fromArray($this->client->post('receipts/global-invoice', $params)); + } + + /** + * Creates a single invoice using a list of receipt keys + * + * @param $data Array of properties and property values for creating an invoice from multiple receipts + * + * @return Invoice The created invoice object + * + * @throws FacturapiException + */ + public function toInvoice(array $data): Invoice + { + try { + return Invoice::fromArray($this->client->post('receipts/to-invoice', $data)); + } catch (FacturapiException $e) { + throw new FacturapiException('Unable to create invoice: ' . $e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Generates an invoice PDF preview for multiple receipts without stamping or saving it + * + * @param array $data Array of properties and property values for generating an invoice PDF preview + * + * @return string PDF file contents + * + * @throws FacturapiException + */ + public function previewToInvoicePdf(array $data): string + { + return $this->client->postRaw('receipts/to-invoice/preview', $data); + } + + /** + * Obtains a signed download URL to preview an invoice for multiple receipts + * + * @param array $params Payload for invoice preview + * @return SignedDownloadUrl + * @throws FacturapiException + */ + public function previewToInvoiceDownloadUrl(array $params): SignedDownloadUrl + { + return SignedDownloadUrl::fromArray($this->client->post('receipts/to-invoice/preview/download-url', $params)); + } + + /** + * Cancel a Receipt + * + * @param string $id Receipt ID. + * @return Receipt JSON-decoded response. + * + * @throws FacturapiException + */ + public function cancel(string $id): Receipt + { + return Receipt::fromArray($this->client->delete('receipts/' . $id)); + } + + /** + * Sends the receipt to the customer's email + * + * @param string $id Receipt ID. + * @param string|array|null $email Email or list of emails. + * @return SuccessResponse JSON-decoded response. + * + * @throws FacturapiException + */ + public function sendByEmail(string $id, string|array|null $email = null): SuccessResponse + { + $payload = []; + if ($email !== null) { + $payload['email'] = is_array($email) ? $email : [$email]; + } + + return SuccessResponse::fromArray($this->client->post('receipts/' . $id . '/email', $payload)); + } + + /** + * @deprecated Use sendByEmail() instead. Will be removed in v5. + */ + public function send_by_email(string $id, string|array|null $email = null): SuccessResponse + { + 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 string $id Receipt ID. + * @return string Raw PDF bytes (binary string, not base64-encoded). + * + * @throws FacturapiException + */ + public function downloadPdf(string $id): string + { + return $this->client->download('receipts/' . $id . '/pdf'); + } + + /** + * @deprecated Use downloadPdf() instead. Will be removed in v5. + */ + public function download_pdf(string $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 10f9a18..ba9a5c7 100644 --- a/src/Resources/Retentions.php +++ b/src/Resources/Retentions.php @@ -1,269 +1,234 @@ executeGetRequest($this->getRequestUrl($params))); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Get a Retention by ID - * - * @param string $id Retention ID. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function retrieve($id): mixed - { - try { - return json_decode($this->executeGetRequest($this->getRequestUrl($id))); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Creates a Retention for the organization - * - * @param array $params Retention payload. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function create($params): mixed - { - try { - return json_decode($this->executeJsonPostRequest($this->getRequestUrl(), $params)); - } catch (FacturapiException $e) { - throw $e; - } - } - - - /** - * Cancels a Retention - * - * @param string $id Retention ID. - * @param array|null $query URL query parameters. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function cancel($id, $query = null): mixed - { - try { - return json_decode($this->executeDeleteRequest($this->getRequestUrl($id, $query), null)); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Updates a Retention with "draft" status - * - * @param string $id Retention ID. - * @param array $body Draft payload. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function updateDraft($id, $body): mixed - { - try { - 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('Retentions::update_draft() is deprecated and will be removed in v5. Use updateDraft() instead.', E_USER_DEPRECATED); - return $this->updateDraft($id, $body); - } - - /** - * Creates a new draft Retention copying the information from the specified Retention - * - * @param string $id Retention ID. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function copyToDraft($id): mixed - { - try { - 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('Retentions::copy_to_draft() is deprecated and will be removed in v5. Use copyToDraft() instead.', E_USER_DEPRECATED); - return $this->copyToDraft($id); - } - - /** - * Stamps a draft Retention - * - * @param string $id Retention ID. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function stampDraft($id): mixed - { - try { - return json_decode($this->executeJsonPostRequest($this->getRequestUrl($id . "/stamp"), null)); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * @deprecated Use stampDraft() instead. Will be removed in v5. - */ - public function stamp_draft($id): mixed - { - trigger_error('Retentions::stamp_draft() is deprecated and will be removed in v5. Use stampDraft() instead.', E_USER_DEPRECATED); - return $this->stampDraft($id); - } - - /** - * Sends the retention to the customer's email - * - * @param string $id Retention 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->executeJsonPostRequest( - $this->getRequestUrl($id) . "/email", - array("email" => $email) - )); - } 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 string $id Retention ID. - * @return string ZIP file contents. - * - * @throws FacturapiException - */ - public function downloadZip($id): string - { - try { - 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 string $id Retention ID. - * @return string Raw PDF bytes (binary string, not base64-encoded). - * - * @throws FacturapiException - */ - public function downloadPdf($id): string - { - try { - 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 string $id Retention ID. - * @return string XML file contents. - * - * @throws FacturapiException - */ - public function downloadXml($id): string - { - try { - 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); - } + public function __construct(private readonly FacturapiHttpClient $client) + { + } + + + /** + * Search Retentions + * + * @param array|null $params Search parameters. + * @return RetentionSearchResult JSON-decoded response. + * + * @throws FacturapiException + */ + public function all(?array $params = null): RetentionSearchResult + { + return RetentionSearchResult::fromArray($this->client->get('retentions', $params)); + } + + /** + * Get a Retention by ID + * + * @param string $id Retention ID. + * @return Retention JSON-decoded response. + * + * @throws FacturapiException + */ + public function retrieve(string $id): Retention + { + return Retention::fromArray($this->client->get('retentions/' . $id)); + } + + /** + * Creates a Retention for the organization + * + * @param array $params Retention payload. + * @return Retention JSON-decoded response. + * + * @throws FacturapiException + */ + public function create(array $params): Retention + { + return Retention::fromArray($this->client->post('retentions', $params)); + } + + + /** + * Cancels a Retention + * + * @param string $id Retention ID. + * @param array|null $query URL query parameters. + * @return Retention JSON-decoded response. + * + * @throws FacturapiException + */ + public function cancel(string $id, ?array $query = null): Retention + { + return Retention::fromArray($this->client->delete('retentions/' . $id, $query)); + } + + /** + * Updates a Retention with "draft" status + * + * @param string $id Retention ID. + * @param array $body Draft payload. + * @return Retention JSON-decoded response. + * + * @throws FacturapiException + */ + public function updateDraft(string $id, array $body): Retention + { + return Retention::fromArray($this->client->put('retentions/' . $id, $body)); + } + + /** + * @deprecated Use updateDraft() instead. Will be removed in v5. + */ + public function update_draft(string $id, array $body): Retention + { + trigger_error('Retentions::update_draft() is deprecated and will be removed in v5. Use updateDraft() instead.', E_USER_DEPRECATED); + return $this->updateDraft($id, $body); + } + + /** + * Creates a new draft Retention copying the information from the specified Retention + * + * @param string $id Retention ID. + * @return Retention JSON-decoded response. + * + * @throws FacturapiException + */ + public function copyToDraft(string $id): Retention + { + return Retention::fromArray($this->client->post('retentions/' . $id . '/copy', [])); + } + + /** + * @deprecated Use copyToDraft() instead. Will be removed in v5. + */ + public function copy_to_draft(string $id): Retention + { + trigger_error('Retentions::copy_to_draft() is deprecated and will be removed in v5. Use copyToDraft() instead.', E_USER_DEPRECATED); + return $this->copyToDraft($id); + } + + /** + * Stamps a draft Retention + * + * @param string $id Retention ID. + * @return Retention JSON-decoded response. + * + * @throws FacturapiException + */ + public function stampDraft(string $id): Retention + { + return Retention::fromArray($this->client->post('retentions/' . $id . '/stamp', [])); + } + + /** + * @deprecated Use stampDraft() instead. Will be removed in v5. + */ + public function stamp_draft(string $id): Retention + { + trigger_error('Retentions::stamp_draft() is deprecated and will be removed in v5. Use stampDraft() instead.', E_USER_DEPRECATED); + return $this->stampDraft($id); + } + + /** + * Sends the retention to the customer's email + * + * @param string $id Retention ID. + * @param string|array|null $email Email or list of emails. + * @return SuccessResponse JSON-decoded response. + * + * @throws FacturapiException + */ + public function sendByEmail(string $id, string|array|null $email = null): SuccessResponse + { + $payload = []; + if ($email !== null) { + $payload['email'] = is_array($email) ? $email : [$email]; + } + + return SuccessResponse::fromArray($this->client->post('retentions/' . $id . '/email', $payload)); + } + + /** + * @deprecated Use sendByEmail() instead. Will be removed in v5. + */ + public function send_by_email(string $id, string|array|null $email = null): SuccessResponse + { + 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 string $id Retention ID. + * @return string ZIP file contents. + * + * @throws FacturapiException + */ + public function downloadZip(string $id): string + { + return $this->client->download('retentions/' . $id . '/zip'); + } + + /** + * @deprecated Use downloadZip() instead. Will be removed in v5. + */ + public function download_zip(string $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 string $id Retention ID. + * @return string Raw PDF bytes (binary string, not base64-encoded). + * + * @throws FacturapiException + */ + public function downloadPdf(string $id): string + { + return $this->client->download('retentions/' . $id . '/pdf'); + } + + /** + * @deprecated Use downloadPdf() instead. Will be removed in v5. + */ + public function download_pdf(string $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 string $id Retention ID. + * @return string XML file contents. + * + * @throws FacturapiException + */ + public function downloadXml(string $id): string + { + return $this->client->download('retentions/' . $id . '/xml'); + } + + /** + * @deprecated Use downloadXml() instead. Will be removed in v5. + */ + public function download_xml(string $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 75c13c7..e924d04 100644 --- a/src/Resources/Tools.php +++ b/src/Resources/Tools.php @@ -1,32 +1,42 @@ client->get('tools/tax_id_validation', ['tax_id' => $tax_id])); + } - /** - * Validates a tax id - * - * @param string $tax_id Tax ID (RFC) to validate. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function validateTaxId($tax_id): mixed - { - try { - return json_decode( - $this->executeGetRequest( - $this->getRequestUrl('tax_id_validation', array('tax_id' => $tax_id)) - ) - ); - } catch (FacturapiException $e) { - throw $e; - } - } + /** + * Health check (Pulse) + * + * @return OkResponse JSON-decoded response. + * + * @throws FacturapiException + */ + public function check(): OkResponse + { + return OkResponse::fromArray($this->client->get('check')); + } } diff --git a/src/Resources/Webhooks.php b/src/Resources/Webhooks.php index 888a5b1..87dde89 100644 --- a/src/Resources/Webhooks.php +++ b/src/Resources/Webhooks.php @@ -1,161 +1,143 @@ executeGetRequest($this->getRequestUrl($params))); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Get a Webhook by ID - * - * @param string $id Webhook ID. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function retrieve($id): mixed - { - try { - return json_decode($this->executeGetRequest($this->getRequestUrl($id))); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Create a Webhook in your organization - * - * @param array $params Webhook payload. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function create($params): mixed - { - try { - return json_decode($this->executeJsonPostRequest($this->getRequestUrl(), $params)); - } catch (FacturapiException $e) { - throw $e; - } - } - - - /** - * Update a Webhook in your organization - * - * @param string $id Webhook ID. - * @param array $params Webhook payload. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function update($id, $params): mixed - { - try { - return json_decode($this->executeJsonPutRequest($this->getRequestUrl($id), $params)); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Delete a Webhook in your organization - * - * @param string $id Webhook ID. - * @return mixed JSON-decoded response. - * - * @throws FacturapiException - */ - public function delete($id): mixed - { - try { - return json_decode($this->executeDeleteRequest($this->getRequestUrl($id), null)); - } catch (FacturapiException $e) { - throw $e; - } - } - - /** - * Validates a webhook signature payload. - * - * Local verification is attempted first when `body`, `signature`, and - * `webhookSecret` are provided in `$data`. If local verification cannot be - * performed for any reason, the SDK falls back to the API endpoint. - * - * @param array $data Signature payload. - * @return mixed JSON-decoded response. - * @throws FacturapiException - */ - public function validateSignature($data): mixed - { - if (is_array($data)) { - try { - $localResult = $this->verifySignatureLocallyFromPayload($data); - if ($localResult !== null) { - return (object) array('valid' => $localResult); - } - } catch (\Throwable $ignored) { - // Fallback to server-side verification. - } - } - - try { - return json_decode($this->executeJsonPostRequest($this->getRequestUrl() . '/validate-signature', $data)); - } catch (FacturapiException $e) { - 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); - } + public function __construct(private readonly FacturapiHttpClient $client) + { + } + + /** + * Get all Webhooks + * + * @param array|null $params Search parameters. + * @return WebhookSearchResult JSON-decoded response. + * + * @throws FacturapiException + */ + public function all(?array $params = null): WebhookSearchResult + { + return WebhookSearchResult::fromArray($this->client->get('webhooks', $params)); + } + + /** + * Get a Webhook by ID + * + * @param string $id Webhook ID. + * @return Webhook JSON-decoded response. + * + * @throws FacturapiException + */ + public function retrieve(string $id): Webhook + { + return Webhook::fromArray($this->client->get('webhooks/' . $id)); + } + + /** + * Create a Webhook in your organization + * + * @param array $params Webhook payload. + * @return Webhook JSON-decoded response. + * + * @throws FacturapiException + */ + public function create(array $params): Webhook + { + return Webhook::fromArray($this->client->post('webhooks', $params)); + } + + + /** + * Update a Webhook in your organization + * + * @param string $id Webhook ID. + * @param array $params Webhook payload. + * @return Webhook JSON-decoded response. + * + * @throws FacturapiException + */ + public function update(string $id, array $params): Webhook + { + return Webhook::fromArray($this->client->put('webhooks/' . $id, $params)); + } + + /** + * Delete a Webhook in your organization + * + * @param string $id Webhook ID. + * @return Webhook JSON-decoded response. + * + * @throws FacturapiException + */ + public function delete(string $id): Webhook + { + return Webhook::fromArray($this->client->delete('webhooks/' . $id)); + } + + /** + * Validates a webhook signature payload. + * + * Local verification is attempted first when `body`, `signature`, and + * `webhookSecret` are provided in `$data`. If local verification cannot be + * performed for any reason, the SDK falls back to the API endpoint. + * + * @param array $data Signature payload. + * @return mixed JSON-decoded response. + * @throws FacturapiException + */ + public function validateSignature($data): mixed + { + if (is_array($data)) { + try { + $localResult = $this->verifySignatureLocallyFromPayload($data); + if ($localResult !== null) { + return ['valid' => $localResult]; + } + } catch (\Throwable) { + // Fallback to server-side verification. + } + } + + return $this->client->post('webhooks/validate-signature', $data); + } + + /** + * 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/src/TaxType.php b/src/TaxType.php deleted file mode 100644 index 906c3cf..0000000 --- a/src/TaxType.php +++ /dev/null @@ -1,9 +0,0 @@ - '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 - { + mockResponse($rawBody, 500, ['Content-Type' => 'application/json']); + + $invoices = test()->facturapi->invoices(); + + expect(fn () => $invoices->create(['customer' => []])) + ->toThrow(function (FacturapiException $exception) use ($rawBody) { + expect($exception->getStatusCode())->toBe(500); + expect($exception->getMessage())->toBe($rawBody); + expect($exception->getErrorData())->toBeNull(); + expect($exception->getRawBody())->toBe($rawBody); + }); + }); + test('empty error body is exposed as empty string', function () { + mockResponse('', 500, ['Content-Type' => 'text/plain']); + + $invoices = test()->facturapi->invoices(); + + expect(fn () => $invoices->create(['customer' => []])) + ->toThrow(function (FacturapiException $exception) { + expect($exception->getStatusCode())->toBe(500); + expect($exception->getMessage())->toBe(''); + expect($exception->getErrorData())->toBeNull(); + expect($exception->getRawBody())->toBe(''); + }); + }); + test('plain text error body is preserved', function () { $rawBody = 'gateway overloaded, retry later'; - $httpClient = new FakeHttpClient( - new Response(503, ['Content-Type' => 'text/plain'], $rawBody) - ); + mockResponse($rawBody, 503, ['Content-Type' => 'text/plain']); + + $invoices = test()->facturapi->invoices(); - $invoices = new Invoices('sk_test_abc123', ['httpClient' => $httpClient]); + expect(fn () => $invoices->create(['customer' => []])) + ->toThrow(function (FacturapiException $exception) use ($rawBody) { + expect($exception->getStatusCode())->toBe(503); + expect($exception->getMessage())->toBe($rawBody); + expect($exception->getErrorData())->toBeNull(); + expect($exception->getRawBody())->toBe($rawBody); + }); + }); - 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 index 61df2e0..17fc2e9 100644 --- a/tests/Http/ErrorHandlingTest.php +++ b/tests/Http/ErrorHandlingTest.php @@ -1,19 +1,10 @@ 'Request validation failed', 'code' => 'validation_error', @@ -36,73 +27,58 @@ public function testApiErrorShapeIsFullyAvailableOnException(): void ], ]; - $httpClient = new FakeHttpClient( - new Response(422, [ + mockResponse(json_encode($errorBody), 422, [ 'Content-Type' => 'application/json', 'Retry-After' => '3', 'x-facturapi-log-id' => 'log_123', - ], 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()); + $invoices = test()->facturapi->invoices(); - self::assertSame('validation_error', $exception->getErrorData()['code']); - self::assertSame('validation_error', $exception->getErrorCode()); - self::assertSame('customer.tax_id', $exception->getErrorPath()); - self::assertSame('body', $exception->getErrorLocation()); - self::assertSame($errorBody['errors'], $exception->getErrors()); - self::assertSame('log_123', $exception->getLogId()); - self::assertSame('3', $exception->getResponseHeaders()['retry-after']); - self::assertSame('log_123', $exception->getResponseHeaders()['x-facturapi-log-id']); - self::assertSame('customer.tax_id', $exception->getErrorData()['details'][0]['path']); - self::assertSame('invalid_rfc', $exception->getErrorData()['details'][0]['code']); - } - } + expect(fn () => $invoices->create(['customer' => []])) + ->toThrow(function (FacturapiException $exception) use ($errorBody) { + expect($exception->getStatusCode())->toBe(422); + expect($exception->getMessage())->toBe('Request validation failed'); + expect($exception->getErrorData())->toBe($errorBody); + expect($exception->getResponseData())->toBe($errorBody); + expect($exception->getRawBody())->toBe(json_encode($errorBody)); - public function testNonJsonErrorsStillExposeRawBody(): void - { + expect($exception->getErrorData()['code'])->toBe('validation_error'); + expect($exception->getErrorCode())->toBe('validation_error'); + expect($exception->getErrorPath())->toBe('customer.tax_id'); + expect($exception->getErrorLocation())->toBe('body'); + expect($exception->getErrors())->toBe($errorBody['errors']); + expect($exception->getLogId())->toBe('log_123'); + expect($exception->getResponseHeaders()['retry-after'])->toBe('3'); + expect($exception->getResponseHeaders()['x-facturapi-log-id'])->toBe('log_123'); + expect($exception->getErrorData()['details'][0]['path'])->toBe('customer.tax_id'); + expect($exception->getErrorData()['details'][0]['code'])->toBe('invalid_rfc'); + }); + }); + test('non json errors still expose raw body', function () { $rawBody = '502 Bad Gateway'; - $httpClient = new FakeHttpClient( - new Response(502, ['Content-Type' => 'text/html'], $rawBody) - ); + mockResponse($rawBody, 502, ['Content-Type' => 'text/html']); - $invoices = new Invoices('sk_test_abc123', ['httpClient' => $httpClient]); + $invoices = test()->facturapi->invoices(); - 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()); - } - } + expect(fn () => $invoices->create(['customer' => []])) + ->toThrow(function (FacturapiException $exception) use ($rawBody) { + expect($exception->getStatusCode())->toBe(502); + expect($exception->getMessage())->toBe($rawBody); + expect($exception->getErrorData())->toBeNull(); + expect($exception->getRawBody())->toBe($rawBody); + }); + }); + test('numeric api error codes are converted to strings', function () { + mockResponse('{"code": 400}', 400, ['Content-Type' => 'application/json']); - public function testNumericApiErrorCodesAreConvertedToStrings(): void - { - $httpClient = new FakeHttpClient( - new Response(400, ['Content-Type' => 'application/json'], '{"code": 400}') - ); + $invoices = test()->facturapi->invoices(); - $invoices = new Invoices('sk_test_abc123', ['httpClient' => $httpClient]); + expect(fn () => $invoices->create(['customer' => []])) + ->toThrow(function (FacturapiException $exception) { + expect($exception->getErrorCode())->toBe('400'); + }); + }); - try { - $invoices->create(['customer' => []]); - self::fail('Expected FacturapiException to be thrown.'); - } catch (FacturapiException $exception) { - self::assertSame('400', $exception->getErrorCode()); - } - } -} +}); diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..446fb21 --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,11 @@ +in('Resources', 'Http'); + +// Add a helper for quickly setting responses +function mockResponse(string $body = '{"ok":true}', int $status = 200, array $headers = []): void +{ + test()->httpClient->addResponse(new \GuzzleHttp\Psr7\Response($status, $headers, $body)); +} diff --git a/tests/ReadmeExamplesTest.php b/tests/ReadmeExamplesTest.php new file mode 100644 index 0000000..13f1fc3 --- /dev/null +++ b/tests/ReadmeExamplesTest.php @@ -0,0 +1,60 @@ +facturapi->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', + ], + ]); + + expect($customer)->toBeInstanceOf(Customer::class) + ->and($customer->email)->toBe('walterwhite@gmail.com') + ->and($customer->legal_name)->toBe('Walter White') + ->and($customer->tax_id)->toBe('WIWA761018') + ->and($customer->address['zip'])->toBe('06800'); + }); + + it('can be configured with custom options (Client Configuration example)', function () { + $facturapi = new Facturapi('fake_key', [ + 'apiVersion' => 'v2', + 'timeout' => 420, + ]); + + expect($facturapi)->toBeInstanceOf(Facturapi::class); + }); +}); diff --git a/tests/Resources/CartaPorteCatalogsTest.php b/tests/Resources/CartaPorteCatalogsTest.php new file mode 100644 index 0000000..d840b86 --- /dev/null +++ b/tests/Resources/CartaPorteCatalogsTest.php @@ -0,0 +1,87 @@ +facturapi->cartaPorteCatalogs(); + $catalogs->searchAirTransportCodes(['q' => 'abc']); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect(urldecode((string) $request->getUri()))->toBe('https://www.facturapi.io/v2/catalogs/cartaporte/3.1/air-transport-codes?q=abc'); + }); + + test('searchTransportConfigs passes query params', function () { + mockResponse('{"data":[]}', 200); + $catalogs = test()->facturapi->cartaPorteCatalogs(); + $catalogs->searchTransportConfigs(['q' => 'abc']); + $request = test()->httpClient->requests()[0]; + expect(urldecode((string) $request->getUri()))->toBe('https://www.facturapi.io/v2/catalogs/cartaporte/3.1/transport-configs?q=abc'); + }); + + test('searchRightsOfPassage passes query params', function () { + mockResponse('{"data":[]}', 200); + $catalogs = test()->facturapi->cartaPorteCatalogs(); + $catalogs->searchRightsOfPassage(['q' => 'abc']); + $request = test()->httpClient->requests()[0]; + expect(urldecode((string) $request->getUri()))->toBe('https://www.facturapi.io/v2/catalogs/cartaporte/3.1/rights-of-passage?q=abc'); + }); + + test('searchCustomsDocuments passes query params', function () { + mockResponse('{"data":[]}', 200); + $catalogs = test()->facturapi->cartaPorteCatalogs(); + $catalogs->searchCustomsDocuments(['q' => 'abc']); + $request = test()->httpClient->requests()[0]; + expect(urldecode((string) $request->getUri()))->toBe('https://www.facturapi.io/v2/catalogs/cartaporte/3.1/customs-documents?q=abc'); + }); + + test('searchPackagingTypes passes query params', function () { + mockResponse('{"data":[]}', 200); + $catalogs = test()->facturapi->cartaPorteCatalogs(); + $catalogs->searchPackagingTypes(['q' => 'abc']); + $request = test()->httpClient->requests()[0]; + expect(urldecode((string) $request->getUri()))->toBe('https://www.facturapi.io/v2/catalogs/cartaporte/3.1/packaging-types?q=abc'); + }); + + test('searchTrailerTypes passes query params', function () { + mockResponse('{"data":[]}', 200); + $catalogs = test()->facturapi->cartaPorteCatalogs(); + $catalogs->searchTrailerTypes(['q' => 'abc']); + $request = test()->httpClient->requests()[0]; + expect(urldecode((string) $request->getUri()))->toBe('https://www.facturapi.io/v2/catalogs/cartaporte/3.1/trailer-types?q=abc'); + }); + + test('searchHazardousMaterials passes query params', function () { + mockResponse('{"data":[]}', 200); + $catalogs = test()->facturapi->cartaPorteCatalogs(); + $catalogs->searchHazardousMaterials(['q' => 'abc']); + $request = test()->httpClient->requests()[0]; + expect(urldecode((string) $request->getUri()))->toBe('https://www.facturapi.io/v2/catalogs/cartaporte/3.1/hazardous-materials?q=abc'); + }); + + test('searchNavalAuthorizations passes query params', function () { + mockResponse('{"data":[]}', 200); + $catalogs = test()->facturapi->cartaPorteCatalogs(); + $catalogs->searchNavalAuthorizations(['q' => 'abc']); + $request = test()->httpClient->requests()[0]; + expect(urldecode((string) $request->getUri()))->toBe('https://www.facturapi.io/v2/catalogs/cartaporte/3.1/naval-authorizations?q=abc'); + }); + + test('searchPortStations passes query params', function () { + mockResponse('{"data":[]}', 200); + $catalogs = test()->facturapi->cartaPorteCatalogs(); + $catalogs->searchPortStations(['q' => 'abc']); + $request = test()->httpClient->requests()[0]; + expect(urldecode((string) $request->getUri()))->toBe('https://www.facturapi.io/v2/catalogs/cartaporte/3.1/port-stations?q=abc'); + }); + + test('searchMarineContainers passes query params', function () { + mockResponse('{"data":[]}', 200); + $catalogs = test()->facturapi->cartaPorteCatalogs(); + $catalogs->searchMarineContainers(['q' => 'abc']); + $request = test()->httpClient->requests()[0]; + expect(urldecode((string) $request->getUri()))->toBe('https://www.facturapi.io/v2/catalogs/cartaporte/3.1/marine-containers?q=abc'); + }); +}); diff --git a/tests/Resources/CatalogsTest.php b/tests/Resources/CatalogsTest.php new file mode 100644 index 0000000..f9f89cc --- /dev/null +++ b/tests/Resources/CatalogsTest.php @@ -0,0 +1,28 @@ +facturapi->catalogs(); + + $catalogs->searchProducts(['q' => 'computadora']); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect(urldecode((string) $request->getUri()))->toBe('https://www.facturapi.io/v2/catalogs/products?q=computadora'); + }); + + test('searchUnits passes query params correctly', function () { + mockResponse('{"data":[]}', 200); + $catalogs = test()->facturapi->catalogs(); + + $catalogs->searchUnits(['q' => 'pieza']); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect(urldecode((string) $request->getUri()))->toBe('https://www.facturapi.io/v2/catalogs/units?q=pieza'); + }); +}); diff --git a/tests/Resources/ComercioExteriorCatalogsTest.php b/tests/Resources/ComercioExteriorCatalogsTest.php new file mode 100644 index 0000000..0ba4f6e --- /dev/null +++ b/tests/Resources/ComercioExteriorCatalogsTest.php @@ -0,0 +1,17 @@ +facturapi->comercioExteriorCatalogs(); + + $catalogs->searchTariffFractions(['q' => '0101']); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect(urldecode((string) $request->getUri()))->toBe('https://www.facturapi.io/v2/catalogs/comercioexterior/2.0/tariff-fractions?q=0101'); + }); +}); diff --git a/tests/Resources/CustomersTest.php b/tests/Resources/CustomersTest.php new file mode 100644 index 0000000..c061fb3 --- /dev/null +++ b/tests/Resources/CustomersTest.php @@ -0,0 +1,414 @@ +facturapi->customers(); + + $payload = [ + 'legal_name' => 'ACME SA DE CV', + 'tax_id' => 'XAXX010101000', + 'tax_system' => '601', + 'email' => 'contact@acme.com', + 'phone' => '5555555555', + 'default_invoice_use' => 'G01', + 'address' => [ + 'zip' => '01000', + 'street' => 'Av. de los Insurgentes Sur', + 'exterior' => '123', + 'interior' => 'Piso 4', + 'neighborhood' => 'Roma Norte', + 'city' => 'Ciudad de México', + 'municipality' => 'Cuauhtémoc', + 'state' => 'DIF', + 'country' => 'MEX', + ], + ]; + + $result = $customers->create($payload, ['createEditLink' => 1]); + + expect($result)->toBeInstanceOf(Customer::class) + ->and($result->id)->toBe('cus_123') + ->and($result->created_at)->toBe('2026-10-12T00:00:00.000Z') + ->and($result->livemode)->toBeFalse() + ->and($result->legal_name)->toBe('ACME SA DE CV') + ->and($result->tax_id)->toBe('XAXX010101000') + ->and($result->tax_system)->toBe('601') + ->and($result->email)->toBe('contact@acme.com') + ->and($result->phone)->toBe('5555555555') + ->and($result->default_invoice_use)->toBe('G01') + ->and($result->address)->toBeInstanceOf(CommonAddressProperties::class) + ->and($result->address->zip)->toBe('01000') + ->and($result->address->street)->toBe('Av. de los Insurgentes Sur') + ->and($result->address->exterior)->toBe('123') + ->and($result->address->interior)->toBe('Piso 4') + ->and($result->address->neighborhood)->toBe('Roma Norte') + ->and($result->address->city)->toBe('Ciudad de México') + ->and($result->address->municipality)->toBe('Cuauhtémoc') + ->and($result->address->state)->toBe('DIF') + ->and($result->address->country)->toBe('MEX') + ->and($result->address['zip'])->toBe('01000') + ->and($result->edit_link)->toBe('https://auto.facturapi.io/tax-info/abcdWXYZ1234') + ->and($result->edit_link_expires_at)->toBe('2026-10-19T00:00:00.000Z') + ->and($result->sat_validated_at)->toBe('2026-10-12T00:00:00.000Z'); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('POST'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/customers?createEditLink=1'); + expect($request->getHeaderLine('Content-Type'))->toBe('application/json'); + + $sentPayload = json_decode((string) $request->getBody(), true); + expect($sentPayload)->toBe($payload); + }); + + test('update sends json body with all editable properties', function () { + $jsonResponse = '{ + "id": "cus_123", + "created_at": "2026-10-12T00:00:00.000Z", + "livemode": false, + "legal_name": "ACME Modificado SA DE CV", + "tax_id": "XAXX010101000", + "tax_system": "606", + "email": "new@acme.com", + "phone": "5551234567", + "default_invoice_use": "G03", + "address": { + "zip": "03100", + "street": "Calle Morelos", + "exterior": "456", + "interior": "Depto 2", + "neighborhood": "Del Valle", + "city": "Ciudad de México", + "municipality": "Benito Juárez", + "state": "CDMX", + "country": "MEX" + }, + "edit_link": null, + "edit_link_expires_at": null, + "sat_validated_at": "2026-10-12T01:00:00.000Z" + }'; + mockResponse($jsonResponse, 200); + $customers = test()->facturapi->customers(); + + $payload = [ + 'legal_name' => 'ACME Modificado SA DE CV', + 'tax_id' => 'XAXX010101000', + 'tax_system' => '606', + 'email' => 'new@acme.com', + 'phone' => '5551234567', + 'default_invoice_use' => 'G03', + 'address' => [ + 'zip' => '03100', + 'street' => 'Calle Morelos', + 'exterior' => '456', + 'interior' => 'Depto 2', + 'neighborhood' => 'Del Valle', + 'city' => 'Ciudad de México', + 'municipality' => 'Benito Juárez', + 'state' => 'CDMX', + 'country' => 'MEX', + ], + ]; + + $result = $customers->update('cus_123', $payload, ['createEditLink' => 0]); + + expect($result)->toBeInstanceOf(Customer::class) + ->and($result->id)->toBe('cus_123') + ->and($result->legal_name)->toBe('ACME Modificado SA DE CV') + ->and($result->tax_id)->toBe('XAXX010101000') + ->and($result->tax_system)->toBe('606') + ->and($result->email)->toBe('new@acme.com') + ->and($result->phone)->toBe('5551234567') + ->and($result->default_invoice_use)->toBe('G03') + ->and($result->address)->toBeInstanceOf(CommonAddressProperties::class) + ->and($result->address->zip)->toBe('03100') + ->and($result->address->street)->toBe('Calle Morelos') + ->and($result->address->exterior)->toBe('456') + ->and($result->address->interior)->toBe('Depto 2') + ->and($result->address->neighborhood)->toBe('Del Valle') + ->and($result->address->city)->toBe('Ciudad de México') + ->and($result->address->municipality)->toBe('Benito Juárez') + ->and($result->address->state)->toBe('CDMX') + ->and($result->address->country)->toBe('MEX') + ->and($result->sat_validated_at)->toBe('2026-10-12T01:00:00.000Z'); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('PUT'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/customers/cus_123?createEditLink=0'); + expect($request->getHeaderLine('Content-Type'))->toBe('application/json'); + + $sentPayload = json_decode((string) $request->getBody(), true); + expect($sentPayload)->toBe($payload); + }); + + test('retrieve fetches a single customer by id with full typed DTO properties', function () { + $jsonResponse = '{ + "id": "cus_123", + "created_at": "2026-10-12T00:00:00.000Z", + "livemode": false, + "legal_name": "ACME SA DE CV", + "tax_id": "XAXX010101000", + "tax_system": "601", + "email": "contact@acme.com", + "phone": "5555555555", + "default_invoice_use": "G01", + "address": { + "zip": "01000", + "street": "Av. de los Insurgentes Sur", + "exterior": "123", + "interior": "Piso 4", + "neighborhood": "Roma Norte", + "city": "Ciudad de México", + "municipality": "Cuauhtémoc", + "state": "DIF", + "country": "MEX" + }, + "edit_link": "https://auto.facturapi.io/tax-info/abcdWXYZ1234", + "edit_link_expires_at": "2026-10-19T00:00:00.000Z", + "sat_validated_at": "2026-10-12T00:00:00.000Z" + }'; + mockResponse($jsonResponse, 200); + $customers = test()->facturapi->customers(); + + $result = $customers->retrieve('cus_123'); + + expect($result)->toBeInstanceOf(Customer::class) + ->and($result->id)->toBe('cus_123') + ->and($result->created_at)->toBe('2026-10-12T00:00:00.000Z') + ->and($result->livemode)->toBeFalse() + ->and($result->legal_name)->toBe('ACME SA DE CV') + ->and($result->tax_id)->toBe('XAXX010101000') + ->and($result->tax_system)->toBe('601') + ->and($result->email)->toBe('contact@acme.com') + ->and($result->phone)->toBe('5555555555') + ->and($result->default_invoice_use)->toBe('G01') + ->and($result->address)->toBeInstanceOf(CommonAddressProperties::class) + ->and($result->address->zip)->toBe('01000') + ->and($result->address->street)->toBe('Av. de los Insurgentes Sur') + ->and($result->address->state)->toBe('DIF') + ->and($result->address->country)->toBe('MEX') + ->and($result->edit_link)->toBe('https://auto.facturapi.io/tax-info/abcdWXYZ1234'); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/customers/cus_123'); + }); + + test('delete removes a customer by id and returns Customer DTO', function () { + $jsonResponse = '{ + "id": "cus_123", + "created_at": "2026-10-12T00:00:00.000Z", + "livemode": false, + "legal_name": "ACME SA DE CV" + }'; + mockResponse($jsonResponse, 200); + $customers = test()->facturapi->customers(); + + $result = $customers->delete('cus_123'); + + expect($result)->toBeInstanceOf(Customer::class) + ->and($result->id)->toBe('cus_123') + ->and($result->legal_name)->toBe('ACME SA DE CV'); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('DELETE'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/customers/cus_123'); + }); + + test('all serializes query parameters correctly and returns CustomerSearchResult', function () { + $jsonResponse = '{ + "page": 1, + "total_pages": 3, + "total_results": 150, + "previous_cursor": null, + "next_cursor": "cur_next_123", + "totals_are_capped": false, + "data": [ + { + "id": "cus_123", + "created_at": "2026-10-12T00:00:00.000Z", + "livemode": false, + "legal_name": "ACME SA DE CV", + "tax_id": "XAXX010101000", + "tax_system": "601", + "email": "contact@acme.com", + "phone": "5555555555", + "default_invoice_use": "G01", + "address": { + "zip": "01000", + "street": "Av. de los Insurgentes Sur", + "state": "DIF", + "country": "MEX" + } + } + ] + }'; + mockResponse($jsonResponse, 200); + $customers = test()->facturapi->customers(); + + $result = $customers->all([ + 'q' => 'acme', + 'date' => [ + 'gte' => '2023-01-01', + 'lte' => '2023-12-31', + ], + 'page' => 1, + 'limit' => 50, + ]); + + expect($result)->toBeInstanceOf(CustomerSearchResult::class) + ->and($result->page)->toBe(1) + ->and($result->total_pages)->toBe(3) + ->and($result->total_results)->toBe(150) + ->and($result->previous_cursor)->toBeNull() + ->and($result->next_cursor)->toBe('cur_next_123') + ->and($result->totals_are_capped)->toBeFalse() + ->and($result->data)->toBeArray()->toHaveCount(1) + ->and($result->data[0])->toBeInstanceOf(Customer::class) + ->and($result->data[0]->id)->toBe('cus_123') + ->and($result->data[0]->legal_name)->toBe('ACME SA DE CV') + ->and($result->data[0]->address)->toBeInstanceOf(CommonAddressProperties::class) + ->and($result->data[0]->address->state)->toBe('DIF') + ->and($result->data[0]->address->country)->toBe('MEX'); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect(urldecode($request->getUri()->getQuery()))->toBe('q=acme&date[gte]=2023-01-01&date[lte]=2023-12-31&page=1&limit=50'); + }); + + test('all serializes cursor and date comparison query parameters correctly', function () { + mockResponse('{"page":1,"total_pages":1,"total_results":0,"data":[]}', 200); + $customers = test()->facturapi->customers(); + + $result = $customers->all([ + 'after' => 'cur_after_abc', + 'before' => 'cur_before_xyz', + 'date' => [ + 'gt' => '2023-01-01', + 'lt' => '2023-06-01', + ], + ]); + + expect($result)->toBeInstanceOf(CustomerSearchResult::class) + ->and($result->data)->toBeArray()->toBeEmpty(); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect(urldecode($request->getUri()->getQuery()))->toBe('after=cur_after_abc&before=cur_before_xyz&date[gt]=2023-01-01&date[lt]=2023-06-01'); + }); + + test('validate tax info returns valid TaxIdValidationResult for valid RFC', function () { + mockResponse('{"is_valid": true, "efos": null, "errors": null}', 200); + $customers = test()->facturapi->customers(); + + $result = $customers->validateTaxInfo('cus_123'); + + expect($result)->toBeInstanceOf(TaxIdValidationResult::class) + ->and($result->is_valid)->toBeTrue() + ->and($result->efos)->toBeNull() + ->and($result->errors)->toBeNull(); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/customers/cus_123/tax-info-validation'); + }); + + test('validate tax info returns invalid TaxIdValidationResult with SAT errors', function () { + $jsonResponse = '{ + "is_valid": false, + "errors": [ + { + "source": "sat", + "code": "tax_id_invalid", + "message": "El RFC no se encuentra registrado en el padrón de contribuyentes" + } + ] + }'; + mockResponse($jsonResponse, 200); + $customers = test()->facturapi->customers(); + + $result = $customers->validateTaxInfo('cus_123'); + + expect($result)->toBeInstanceOf(TaxIdValidationResult::class) + ->and($result->is_valid)->toBeFalse() + ->and($result->errors)->toBeArray()->toHaveCount(1) + ->and($result->errors[0]['source'])->toBe('sat') + ->and($result->errors[0]['code'])->toBe('tax_id_invalid') + ->and($result->errors[0]['message'])->toBe('El RFC no se encuentra registrado en el padrón de contribuyentes'); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/customers/cus_123/tax-info-validation'); + }); + + test('send edit link by email sends correct payload and returns OkResponse', function () { + mockResponse('{"ok":true}', 200); + $customers = test()->facturapi->customers(); + + $payload = ['email' => 'edit@acme.com']; + $result = $customers->sendEditLink('cus_123', $payload); + + expect($result)->toBeInstanceOf(OkResponse::class) + ->and($result->ok)->toBeTrue() + ->and($result->success)->toBeTrue(); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('POST'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/customers/cus_123/email-edit-link'); + + $sentPayload = json_decode((string) $request->getBody(), true); + expect($sentPayload)->toBe($payload); + }); + + test('send edit link with default empty payload sends empty body and returns OkResponse', function () { + mockResponse('{"ok":true}', 200); + $customers = test()->facturapi->customers(); + + $result = $customers->sendEditLink('cus_123'); + + expect($result)->toBeInstanceOf(OkResponse::class) + ->and($result->ok)->toBeTrue() + ->and($result->success)->toBeTrue(); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('POST'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/customers/cus_123/email-edit-link'); + + $sentPayload = (string) $request->getBody(); + expect($sentPayload)->toBe(''); + }); +}); diff --git a/tests/Resources/InvoiceReceiptCreationTest.php b/tests/Resources/InvoiceReceiptCreationTest.php index 9c6e547..3170b11 100644 --- a/tests/Resources/InvoiceReceiptCreationTest.php +++ b/tests/Resources/InvoiceReceiptCreationTest.php @@ -1,22 +1,12 @@ $httpClient]); +describe('InvoiceReceiptCreation', function () { + test('invoices create sends json body and query params', function () { + mockResponse('{"id":"inv_123"}', 201); + $invoices = test()->facturapi->invoices(); $payload = [ 'customer' => 'cus_123', @@ -25,19 +15,17 @@ public function testInvoicesCreateSendsJsonBodyAndQueryParams(): void $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()); - } + expect($result->id)->toBe('inv_123'); - public function testReceiptsCreateSendsJsonBodyToExpectedEndpoint(): void - { - $httpClient = new FakeHttpClient(new Response(201, [], '{"id":"rec_123"}')); - $receipts = new Receipts('sk_test_abc123', ['httpClient' => $httpClient]); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('POST'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/invoices?as=draft&validate=true'); + expect($request->getHeaderLine('Content-Type'))->toBe('application/json'); + expect((string) $request->getBody())->toBe(json_encode($payload)); + }); + test('receipts create sends json body to expected endpoint', function () { + mockResponse('{"id":"rec_123"}', 201); + $receipts = test()->facturapi->receipts(); $payload = [ 'customer' => 'cus_123', @@ -46,31 +34,24 @@ public function testReceiptsCreateSendsJsonBodyToExpectedEndpoint(): void $result = $receipts->create($payload); - self::assertSame('rec_123', $result->id); + expect($result->id)->toBe('rec_123'); - $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]); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('POST'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/receipts'); + expect($request->getHeaderLine('Content-Type'))->toBe('application/json'); + expect((string) $request->getBody())->toBe(json_encode($payload)); + }); + test('invoices cancel serializes query params without extra slash', function () { + mockResponse('{"status":"canceled"}', 200); + $invoices = test()->facturapi->invoices(); $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 - { + $request = test()->httpClient->requests()[0]; + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/invoices/inv_123?motive=01&substitution=inv_456'); + }); + test('invoices cancel throws on non2xx and preserves error shape', function () { $errorBody = [ 'message' => 'Cancellation rejected', 'code' => 'cancel_error', @@ -79,18 +60,15 @@ public function testInvoicesCancelThrowsOnNon2xxAndPreservesErrorShape(): void ], ]; - $httpClient = new FakeHttpClient( - new Response(409, ['Content-Type' => 'application/json'], json_encode($errorBody)) - ); - $invoices = new Invoices('sk_test_abc123', ['httpClient' => $httpClient]); + mockResponse(json_encode($errorBody), 409, ['Content-Type' => 'application/json']); + $invoices = test()->facturapi->invoices(); + + expect(fn () => $invoices->cancel('inv_123', ['motive' => '99'])) + ->toThrow(function (FacturapiException $exception) use ($errorBody) { + expect($exception->getStatusCode())->toBe(409); + expect($exception->getMessage())->toBe('Cancellation rejected'); + expect($exception->getErrorData())->toBe($errorBody); + }); + }); - 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 index a7012f5..f843ee2 100644 --- a/tests/Resources/InvoicesTest.php +++ b/tests/Resources/InvoicesTest.php @@ -1,45 +1,122 @@ toBeTrue(); + }); + + test('create sends full json body with all required and optional properties', function () { + mockResponse(json_encode([ + 'id' => 'inv_123', + 'created_at' => '2025-03-01T12:00:00Z', + 'livemode' => false, + 'status' => 'valid', + 'payment_method' => 'PUE', + 'use' => 'G01', + 'conditions' => 'Inmediato', + 'currency' => 'MXN', + ]), 201); + $invoices = test()->facturapi->invoices(); -namespace Facturapi\Tests\Resources; + $payload = [ + 'customer' => 'cus_123', + 'items' => [ + [ + 'quantity' => 2, + 'product' => [ + 'description' => 'Guitarra', + 'product_key' => '60131324', + 'price' => 345.60, + 'tax_included' => true, + 'taxes' => [ + ['type' => 'IVA', 'rate' => 0.16] + ] + ] + ] + ], + 'use' => 'G01', + 'payment_form' => '01', + 'payment_method' => 'PUE', + 'currency' => 'MXN', + 'exchange' => 1.5, + 'conditions' => 'Inmediato', + 'related' => [ + 'relation' => '01', + 'invoices' => ['inv_001', 'inv_002'] + ], + 'pdf_custom_section' => 'Custom text here', + 'addenda' => 'XML string', + 'namespaced_addenda' => [['namespace' => 'x', 'xml' => 'y']] + ]; -use Facturapi\Resources\Invoices; -use Facturapi\Tests\Support\FakeHttpClient; -use GuzzleHttp\Psr7\Response; -use PHPUnit\Framework\TestCase; + $result = $invoices->create($payload); + + expect($result->id)->toBe('inv_123'); + expect($result->payment_method)->toBe('PUE'); + expect($result->use)->toBe('G01'); + expect($result->conditions)->toBe('Inmediato'); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('POST'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/invoices'); + + $sentPayload = json_decode((string) $request->getBody(), true); + expect($sentPayload)->toBe($payload); + }); + + test('updateDraft sends json body with CFDI 4.0 standard fields and returns Invoice DTO', function () { + mockResponse(json_encode([ + 'id' => 'inv_123', + 'created_at' => '2025-03-01T12:00:00Z', + 'livemode' => false, + 'status' => 'draft', + 'payment_method' => 'PUE', + 'use' => 'G01', + 'conditions' => 'Inmediato', + 'currency' => 'MXN', + ]), 200); + $invoices = test()->facturapi->invoices(); -final class InvoicesTest extends TestCase -{ - public function testGetLastStatusIsNullBeforeAnyRequest(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], '{}')); - $invoices = new Invoices('sk_test_abc123', ['httpClient' => $httpClient]); + $payload = [ + 'payment_method' => 'PUE', + 'use' => 'G01', + 'conditions' => 'Inmediato', + 'payment_form' => '03', + 'currency' => 'MXN', + ]; - self::assertNull($invoices->getLastStatus()); - } + $result = $invoices->updateDraft('inv_123', $payload); - public function testDownloadPdfUsesExpectedPathAndAuthorizationHeader(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], 'PDF_BINARY_CONTENT')); - $invoices = new Invoices('sk_test_abc123', ['httpClient' => $httpClient]); + expect($result->id)->toBe('inv_123'); + expect($result->payment_method)->toBe('PUE'); + expect($result->use)->toBe('G01'); + expect($result->conditions)->toBe('Inmediato'); - $result = $invoices->downloadPdf('inv_123'); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('PUT'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/invoices/inv_123'); - self::assertSame('PDF_BINARY_CONTENT', $result); + $sentPayload = json_decode((string) $request->getBody(), true); + expect($sentPayload)->toBe($payload); + }); + test('download pdf uses expected path and authorization header', function () { + mockResponse('PDF_BINARY_CONTENT', 200); + $invoices = test()->facturapi->invoices(); + + $result = $invoices->downloadPdf('inv_123'); - $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()); - } + expect($result)->toBe('PDF_BINARY_CONTENT'); - public function testAllSerializesNestedDateRangeQueryParametersIntoUrl(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], '{"data":[]}')); - $invoices = new Invoices('sk_test_abc123', ['httpClient' => $httpClient]); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/invoices/inv_123/pdf'); + expect($request->getHeaderLine('Authorization'))->toBe('Basic ' . base64_encode('sk_test_abc123:')); + expect($request->getHeaderLine('User-Agent'))->toBe('facturapi-php'); + }); + test('all serializes nested date range query parameters into url', function () { + mockResponse('{"data":[]}', 200); + $invoices = test()->facturapi->invoices(); $invoices->all([ 'q' => 'XAXX010101000', @@ -49,77 +126,58 @@ public function testAllSerializesNestedDateRangeQueryParametersIntoUrl(): void ], ]); - $request = $httpClient->requests()[0]; - self::assertSame('https://www.facturapi.io/v2/invoices', $request->getUri()->getScheme() . '://' . $request->getUri()->getHost() . $request->getUri()->getPath()); - self::assertSame( - 'q=XAXX010101000&date[gte]=2019-03-01&date[lte]=2019-03-31', - urldecode($request->getUri()->getQuery()) - ); - } - - public function testAllRepeatsTheKeyForListQueryParameters(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], '{"data":[]}')); - $invoices = new Invoices('sk_test_abc123', ['httpClient' => $httpClient]); + $request = test()->httpClient->requests()[0]; + expect($request->getUri()->getScheme() . '://' . $request->getUri()->getHost() . $request->getUri()->getPath())->toBe('https://www.facturapi.io/v2/invoices'); + expect(urldecode($request->getUri()->getQuery()))->toBe('q=XAXX010101000&date[gte]=2019-03-01&date[lte]=2019-03-31'); + }); + test('all repeats the key for list query parameters', function () { + mockResponse('{"data":[]}', 200); + $invoices = test()->facturapi->invoices(); $invoices->all([ 'status' => ['valid', 'canceled'], ]); - $request = $httpClient->requests()[0]; - self::assertSame( - 'status=valid&status=canceled', - urldecode($request->getUri()->getQuery()) - ); - } - - public function testAllOmitsNullQueryParameters(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], '{"data":[]}')); - $invoices = new Invoices('sk_test_abc123', ['httpClient' => $httpClient]); + $request = test()->httpClient->requests()[0]; + expect(urldecode($request->getUri()->getQuery()))->toBe('status=valid&status=canceled'); + }); + test('all omits null query parameters', function () { + mockResponse('{"data":[]}', 200); + $invoices = test()->facturapi->invoices(); $invoices->all([ 'q' => null, 'page' => 2, ]); - $request = $httpClient->requests()[0]; - self::assertSame('page=2', urldecode($request->getUri()->getQuery())); - } - - public function testAllSendsExplicitEmptyQueryValues(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], '{"data":[]}')); - $invoices = new Invoices('sk_test_abc123', ['httpClient' => $httpClient]); + $request = test()->httpClient->requests()[0]; + expect(urldecode($request->getUri()->getQuery()))->toBe('page=2'); + }); + test('all sends explicit empty query values', function () { + mockResponse('{"data":[]}', 200); + $invoices = test()->facturapi->invoices(); $invoices->all([ 'q' => '', ]); - $request = $httpClient->requests()[0]; - self::assertSame('q=', urldecode($request->getUri()->getQuery())); - } - - public function testAllUrlEncodesScalarQueryValues(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], '{"data":[]}')); - $invoices = new Invoices('sk_test_abc123', ['httpClient' => $httpClient]); + $request = test()->httpClient->requests()[0]; + expect(urldecode($request->getUri()->getQuery()))->toBe('q='); + }); + test('all url encodes scalar query values', function () { + mockResponse('{"data":[]}', 200); + $invoices = test()->facturapi->invoices(); $invoices->all([ 'q' => 'ACME SA de CV', ]); - $request = $httpClient->requests()[0]; - self::assertSame( - 'https://www.facturapi.io/v2/invoices?q=ACME+SA+de+CV', - (string) $request->getUri() - ); - } - - public function testCreateZipRequestUsesExpectedPathAndJsonBody(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], '{"id":"zip_123"}')); - $invoices = new Invoices('sk_test_abc123', ['httpClient' => $httpClient]); + $request = test()->httpClient->requests()[0]; + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/invoices?q=ACME+SA+de+CV'); + }); + test('create zip request uses expected path and json body', function () { + mockResponse('{"id":"zip_123","created_at":"2025-03-01T00:00:00.000Z","livemode":false,"organization":"org_123","issuer_type":"issuing","invoice_types":[],"start_date":"2025-03-01T00:00:00.000Z","end_date":"2025-03-31T00:00:00.000Z","status":"finished"}', 200); + $invoices = test()->facturapi->invoices(); $payload = [ 'year' => 2025, 'month' => 3, @@ -129,18 +187,16 @@ public function testCreateZipRequestUsesExpectedPathAndJsonBody(): void $result = $invoices->createZipRequest($payload); - self::assertSame('zip_123', $result->id); - $request = $httpClient->requests()[0]; - self::assertSame('POST', $request->getMethod()); - self::assertSame('https://www.facturapi.io/v2/invoices/zip-requests', (string) $request->getUri()); - self::assertSame('application/json', $request->getHeaderLine('Content-Type')); - self::assertSame($payload, json_decode((string) $request->getBody(), true)); - } - - public function testListZipRequestsUsesExpectedPathAndQuery(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], '{"data":[]}')); - $invoices = new Invoices('sk_test_abc123', ['httpClient' => $httpClient]); + expect($result->id)->toBe('zip_123'); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('POST'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/invoices/zip-requests'); + expect($request->getHeaderLine('Content-Type'))->toBe('application/json'); + expect(json_decode((string) $request->getBody(), true))->toBe($payload); + }); + test('list zip requests uses expected path and query', function () { + mockResponse('{"data":[]}', 200); + $invoices = test()->facturapi->invoices(); $result = $invoices->listZipRequests([ 'year' => 2025, @@ -150,43 +206,34 @@ public function testListZipRequestsUsesExpectedPathAndQuery(): void 'page' => 1, ]); - self::assertSame([], $result->data); - $request = $httpClient->requests()[0]; - self::assertSame('GET', $request->getMethod()); - self::assertSame( - 'https://www.facturapi.io/v2/invoices/zip-requests?year=2025&month=3&status=finished&limit=20&page=1', - (string) $request->getUri() - ); - } - - public function testRetrieveZipRequestUsesExpectedPath(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], '{"id":"zip_123"}')); - $invoices = new Invoices('sk_test_abc123', ['httpClient' => $httpClient]); + expect($result->data)->toBe([]); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/invoices/zip-requests?year=2025&month=3&status=finished&limit=20&page=1'); + }); + test('retrieve zip request uses expected path', function () { + mockResponse('{"id":"zip_123","created_at":"2025-03-01T00:00:00.000Z","livemode":false,"organization":"org_123","issuer_type":"issuing","invoice_types":[],"start_date":"2025-03-01T00:00:00.000Z","end_date":"2025-03-31T00:00:00.000Z","status":"finished"}', 200); + $invoices = test()->facturapi->invoices(); $result = $invoices->retrieveZipRequest('zip_123'); - self::assertSame('zip_123', $result->id); - $request = $httpClient->requests()[0]; - self::assertSame('GET', $request->getMethod()); - self::assertSame('https://www.facturapi.io/v2/invoices/zip-requests/zip_123', (string) $request->getUri()); - } - - public function testDownloadZipRequestReturnsBinaryContentsFromExpectedPath(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], 'ZIP_BINARY_CONTENT')); - $invoices = new Invoices('sk_test_abc123', ['httpClient' => $httpClient]); + expect($result->id)->toBe('zip_123'); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/invoices/zip-requests/zip_123'); + }); + test('download zip request returns binary contents from expected path', function () { + mockResponse('ZIP_BINARY_CONTENT', 200); + $invoices = test()->facturapi->invoices(); $result = $invoices->downloadZipRequest('zip_123'); - self::assertSame('ZIP_BINARY_CONTENT', $result); - $request = $httpClient->requests()[0]; - self::assertSame('GET', $request->getMethod()); - self::assertSame('https://www.facturapi.io/v2/invoices/zip-requests/zip_123/zip', (string) $request->getUri()); - } - - public function testPaymentSummaryUsesExpectedPathAndQueryParameter(): void - { + expect($result)->toBe('ZIP_BINARY_CONTENT'); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/invoices/zip-requests/zip_123/zip'); + }); + test('payment summary uses expected path and query parameter', function () { $summary = [ 'uuid' => '6CF6CE33-1BD2-4F88-A443-33013C069169', 'installment' => 1, @@ -198,18 +245,63 @@ public function testPaymentSummaryUsesExpectedPathAndQueryParameter(): void ['base' => 50.0, 'rate' => 0.16, 'type' => 'IVA', 'factor' => 'Tasa', 'withholding' => false], ], ]; - $httpClient = new FakeHttpClient(new Response(200, [], json_encode($summary))); - $invoices = new Invoices('sk_test_abc123', ['httpClient' => $httpClient]); + mockResponse(json_encode($summary), 200); + $invoices = test()->facturapi->invoices(); $result = $invoices->paymentSummary('inv_123', 58.0); - self::assertSame('6CF6CE33-1BD2-4F88-A443-33013C069169', $result->uuid); - self::assertSame(1, $result->installment); - self::assertSame(50.0, (float) $result->taxes[0]->base); + expect($result->uuid)->toBe('6CF6CE33-1BD2-4F88-A443-33013C069169'); + expect($result->installment)->toBe(1); + expect((float) $result->taxes[0]['base'])->toBe(50.0); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/invoices/inv_123/payment-summary?amount=58'); + expect($request->getHeaderLine('Authorization'))->toBe('Basic ' . base64_encode('sk_test_abc123:')); + }); + + test('sendByEmail sends single email string to customer and returns SuccessResponse', function () { + mockResponse('{"success":true}', 200); + $invoices = test()->facturapi->invoices(); + + $result = $invoices->sendByEmail('inv_123', 'customer@example.com'); + + expect($result->success)->toBeTrue(); + expect($result->ok)->toBeTrue(); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('POST'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/invoices/inv_123/email'); + expect(json_decode((string) $request->getBody(), true))->toBe(['email' => ['customer@example.com']]); + }); + + test('sendByEmail sends email array to customer and returns SuccessResponse', function () { + mockResponse('{"success":true}', 200); + $invoices = test()->facturapi->invoices(); + + $result = $invoices->sendByEmail('inv_123', ['user1@example.com', 'user2@example.com']); + + expect($result->success)->toBeTrue(); + expect($result->ok)->toBeTrue(); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('POST'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/invoices/inv_123/email'); + expect(json_decode((string) $request->getBody(), true))->toBe(['email' => ['user1@example.com', 'user2@example.com']]); + }); + + test('sendByEmail sends empty payload when email is null and returns SuccessResponse', function () { + mockResponse('{"success":true}', 200); + $invoices = test()->facturapi->invoices(); + + $result = $invoices->sendByEmail('inv_123'); + + expect($result->success)->toBeTrue(); + expect($result->ok)->toBeTrue(); - $request = $httpClient->requests()[0]; - self::assertSame('GET', $request->getMethod()); - self::assertSame('https://www.facturapi.io/v2/invoices/inv_123/payment-summary?amount=58', (string) $request->getUri()); - self::assertSame('Basic ' . base64_encode('sk_test_abc123:'), $request->getHeaderLine('Authorization')); - } -} + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('POST'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/invoices/inv_123/email'); + expect((string) $request->getBody())->toBe(''); + }); +}); diff --git a/tests/Resources/NominaCatalogsTest.php b/tests/Resources/NominaCatalogsTest.php new file mode 100644 index 0000000..c14f28b --- /dev/null +++ b/tests/Resources/NominaCatalogsTest.php @@ -0,0 +1,28 @@ +facturapi->nominaCatalogs(); + + $catalogs->searchDeductions(['q' => 'isr']); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect(urldecode((string) $request->getUri()))->toBe('https://www.facturapi.io/v2/catalogs/nomina/deductions?q=isr'); + }); + + test('searchPerceptions passes query params correctly', function () { + mockResponse('{"data":[]}', 200); + $catalogs = test()->facturapi->nominaCatalogs(); + + $catalogs->searchPerceptions(['q' => 'sueldo']); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect(urldecode((string) $request->getUri()))->toBe('https://www.facturapi.io/v2/catalogs/nomina/perceptions?q=sueldo'); + }); +}); diff --git a/tests/Resources/OrganizationsDomainTest.php b/tests/Resources/OrganizationsDomainTest.php index e6b757e..e49d1af 100644 --- a/tests/Resources/OrganizationsDomainTest.php +++ b/tests/Resources/OrganizationsDomainTest.php @@ -1,61 +1,40 @@ $httpClient]); +describe('OrganizationsDomain', function () { + test('all serializes query parameters into url', function () { + mockResponse('{"data":[]}', 200); + $organizations = test()->facturapi->organizations(); $organizations->all([ 'q' => 'XAXX010101000', ]); - $request = $httpClient->requests()[0]; - self::assertSame('GET', $request->getMethod()); - self::assertSame( - 'https://www.facturapi.io/v2/organizations?q=XAXX010101000', - (string) $request->getUri() - ); - } - - public function testCheckDomainAvailabilityUsesExpectedEndpoint(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], '{"available":true}')); - $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations?q=XAXX010101000'); + }); + test('check domain availability uses expected endpoint', function () { + mockResponse('{"available":true}', 200); + $organizations = test()->facturapi->organizations(); $result = $organizations->checkDomainAvailability([ 'name' => 'acme', 'domain' => 'acme.mx', ]); - self::assertTrue($result->available); - - $request = $httpClient->requests()[0]; - self::assertSame('GET', $request->getMethod()); - self::assertSame( - 'https://www.facturapi.io/v2/organizations/domain-check?name=acme&domain=acme.mx', - (string) $request->getUri() - ); - } + expect($result->available)->toBeTrue(); - public function testCheckDomainAvailabilityThrowsWhenParamsAreNotArray(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], '{"available":true}')); - $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations/domain-check?name=acme&domain=acme.mx'); + }); + test('check domain availability throws when params are not array', function () { + mockResponse('{"available":true}', 200); + $organizations = test()->facturapi->organizations(); - $this->expectException(\Facturapi\Exceptions\FacturapiException::class); - $this->expectExceptionMessage('checkDomainAvailability expects $query to be an array.'); + expect(fn () => $organizations->checkDomainAvailability('invalid')) + ->toThrow(\Facturapi\Exceptions\FacturapiException::class, 'checkDomainAvailability expects $query to be an array.'); + }); - $organizations->checkDomainAvailability('invalid'); - } -} +}); diff --git a/tests/Resources/OrganizationsMultipartTest.php b/tests/Resources/OrganizationsMultipartTest.php index 746433b..fa49b93 100644 --- a/tests/Resources/OrganizationsMultipartTest.php +++ b/tests/Resources/OrganizationsMultipartTest.php @@ -1,106 +1,80 @@ $httpClient]); + mockResponse('{"id":"org_123"}', 200); + $organizations = test()->facturapi->organizations(); - $result = $organizations->uploadLogo('org_123', $tmpLogo); + $result = $organizations->uploadLogo('org_123', $tmpLogo); - self::assertTrue($result->ok); + expect($result->id)->toBe('org_123'); - $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')); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('PUT'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations/org_123/logo'); + expect($request->getHeaderLine('Content-Type'))->toStartWith('multipart/form-data; boundary='); - $body = (string) $request->getBody(); - self::assertStringContainsString('name="file"', $body); - self::assertStringContainsString('filename="' . basename($tmpLogo) . '"', $body); - self::assertStringContainsString('LOGO_BYTES', $body); - } finally { - @unlink($tmpLogo); - } - } + $body = (string) $request->getBody(); + expect($body)->toContain('name="file"'); + expect($body)->toContain('filename="' . basename($tmpLogo) . '"'); + expect($body)->toContain('LOGO_BYTES'); - public function testUploadCertificateBuildsMultipartRequestWithCerKeyAndPassword(): void - { + @unlink($tmpLogo); + }); + test('upload certificate builds multipart request with cer key and password', function () { $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'); - try { - $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); - } finally { - @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', + mockResponse('{"id":"org_123"}', 200); + $organizations = test()->facturapi->organizations(); + + $result = $organizations->uploadCertificate('org_123', [ + 'cerFile' => $tmpCer, + 'keyFile' => $tmpKey, + 'password' => 'secret_password', ]); - } - public function testUploadLogoFailsWhenFileCannotBeOpened(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], '{"ok":true}')); - $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + expect($result->id)->toBe('org_123'); - $this->expectException(FacturapiException::class); - $this->expectExceptionMessage('Unable to open file: /tmp/file_that_does_not_exist.pdf'); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('PUT'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations/org_123/certificate'); - $organizations->uploadLogo('org_123', '/tmp/file_that_does_not_exist.pdf'); - } + $body = (string) $request->getBody(); + expect($body)->toContain('name="cer"'); + expect($body)->toContain('name="key"'); + expect($body)->toContain('name="password"'); + expect($body)->toContain('CER_BYTES'); + expect($body)->toContain('KEY_BYTES'); + expect($body)->toContain('secret_password'); - public function testUploadLogoThrowsOnNon2xxAndPreservesErrorShape(): void - { + @unlink($tmpCer); + @unlink($tmpKey); + }); + test('upload certificate fails when required fields are missing', function () { + mockResponse('{"id":"org_123"}', 200); + $organizations = test()->facturapi->organizations(); + + expect(fn () => $organizations->uploadCertificate('org_123', [ + 'cerFile' => '/tmp/cert.cer', + ]))->toThrow(FacturapiException::class, 'Invalid certificate payload. Expected cerFile, keyFile and password.'); + }); + test('upload logo fails when file cannot be opened', function () { + mockResponse('{"id":"org_123"}', 200); + $organizations = test()->facturapi->organizations(); + + expect(fn () => $organizations->uploadLogo('org_123', '/tmp/file_that_does_not_exist.pdf')) + ->toThrow(FacturapiException::class, 'Unable to open file: /tmp/file_that_does_not_exist.pdf'); + }); + test('upload logo throws on non2xx and preserves error shape', function () { $tmpLogo = tempnam(sys_get_temp_dir(), 'logo_'); file_put_contents($tmpLogo, 'LOGO_BYTES'); @@ -112,20 +86,17 @@ public function testUploadLogoThrowsOnNon2xxAndPreservesErrorShape(): void ], ]; - $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); - } - } -} + mockResponse(json_encode($errorBody), 422, ['Content-Type' => 'application/json']); + $organizations = test()->facturapi->organizations(); + + expect(fn () => $organizations->uploadLogo('org_123', $tmpLogo)) + ->toThrow(function (FacturapiException $exception) use ($errorBody) { + expect($exception->getStatusCode())->toBe(422); + expect($exception->getMessage())->toBe('Upload failed'); + expect($exception->getErrorData())->toBe($errorBody); + }); + + @unlink($tmpLogo); + }); + +}); diff --git a/tests/Resources/OrganizationsTeamManagementTest.php b/tests/Resources/OrganizationsTeamManagementTest.php index 3424d13..d1ec25f 100644 --- a/tests/Resources/OrganizationsTeamManagementTest.php +++ b/tests/Resources/OrganizationsTeamManagementTest.php @@ -1,243 +1,204 @@ $httpClient]); +describe('OrganizationsTeamManagement', function () { + test('list team access uses expected endpoint', function () { + mockResponse('[]', 200); + $organizations = test()->facturapi->organizations(); $result = $organizations->listTeamAccess('org_123'); - self::assertIsArray($result); - $request = $httpClient->requests()[0]; - self::assertSame('GET', $request->getMethod()); - self::assertSame('https://www.facturapi.io/v2/organizations/org_123/team', (string) $request->getUri()); - } - - public function testRetrieveTeamAccessUsesExpectedEndpoint(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], '{"id":"acc_123"}')); - $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + expect($result)->toBeArray(); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations/org_123/team'); + }); + test('retrieve team access uses expected endpoint', function () { + mockResponse('{"id":"acc_123"}', 200); + $organizations = test()->facturapi->organizations(); $result = $organizations->retrieveTeamAccess('org_123', 'acc_123'); - self::assertSame('acc_123', $result->id); - $request = $httpClient->requests()[0]; - self::assertSame('GET', $request->getMethod()); - self::assertSame('https://www.facturapi.io/v2/organizations/org_123/team/acc_123', (string) $request->getUri()); - } - - public function testInviteUserToTeamUsesExpectedEndpointAndJsonBody(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], '{"id":"inv_123"}')); - $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + expect($result['id'])->toBe('acc_123'); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations/org_123/team/acc_123'); + }); + test('invite user to team uses expected endpoint and json body', function () { + mockResponse('{"id":"inv_123"}', 200); + $organizations = test()->facturapi->organizations(); $result = $organizations->inviteUserToTeam('org_123', [ 'email' => 'alex@example.com', 'role' => 'role_123', ]); - self::assertSame('inv_123', $result->id); - $request = $httpClient->requests()[0]; - self::assertSame('POST', $request->getMethod()); - self::assertSame('https://www.facturapi.io/v2/organizations/org_123/team/invites', (string) $request->getUri()); - self::assertSame('application/json', $request->getHeaderLine('Content-Type')); - self::assertStringContainsString('"email":"alex@example.com"', (string) $request->getBody()); - self::assertStringContainsString('"role":"role_123"', (string) $request->getBody()); - } - - public function testListSentTeamInvitesUsesExpectedEndpoint(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], '[]')); - $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + expect($result['id'])->toBe('inv_123'); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('POST'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations/org_123/team/invites'); + expect($request->getHeaderLine('Content-Type'))->toBe('application/json'); + expect((string) $request->getBody())->toContain('"email":"alex@example.com"'); + expect((string) $request->getBody())->toContain('"role":"role_123"'); + }); + test('list sent team invites uses expected endpoint', function () { + mockResponse('[]', 200); + $organizations = test()->facturapi->organizations(); $result = $organizations->listSentTeamInvites('org_123'); - self::assertIsArray($result); - $request = $httpClient->requests()[0]; - self::assertSame('GET', $request->getMethod()); - self::assertSame('https://www.facturapi.io/v2/organizations/org_123/team/invites', (string) $request->getUri()); - } - - public function testCancelTeamInviteUsesExpectedEndpoint(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], '{"ok":true}')); - $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + expect($result)->toBeArray(); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations/org_123/team/invites'); + }); + test('cancel team invite uses expected endpoint', function () { + mockResponse('{"ok":true}', 200); + $organizations = test()->facturapi->organizations(); $result = $organizations->cancelTeamInvite('org_123', 'inv_123'); - self::assertTrue($result->ok); - $request = $httpClient->requests()[0]; - self::assertSame('DELETE', $request->getMethod()); - self::assertSame('https://www.facturapi.io/v2/organizations/org_123/team/invites/inv_123', (string) $request->getUri()); - } - - public function testListReceivedTeamInvitesUsesExpectedEndpoint(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], '[]')); - $organizations = new Organizations('sk_user_abc123', ['httpClient' => $httpClient]); + expect($result['ok'])->toBeTrue(); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('DELETE'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations/org_123/team/invites/inv_123'); + }); + test('list received team invites uses expected endpoint', function () { + mockResponse('[]', 200); + $organizations = test()->facturapi->organizations(); $result = $organizations->listReceivedTeamInvites(); - self::assertIsArray($result); - $request = $httpClient->requests()[0]; - self::assertSame('GET', $request->getMethod()); - self::assertSame('https://www.facturapi.io/v2/organizations/invites/pending', (string) $request->getUri()); - } - - public function testRespondTeamInviteUsesExpectedEndpointAndJsonBody(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], '{"ok":true}')); - $organizations = new Organizations('sk_user_abc123', ['httpClient' => $httpClient]); + expect($result)->toBeArray(); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations/invites/pending'); + }); + test('respond team invite uses expected endpoint and json body', function () { + mockResponse('{"ok":true}', 200); + $organizations = test()->facturapi->organizations(); $result = $organizations->respondTeamInvite('inv_123', ['accept' => true]); - self::assertTrue($result->ok); - $request = $httpClient->requests()[0]; - self::assertSame('POST', $request->getMethod()); - self::assertSame('https://www.facturapi.io/v2/organizations/invites/inv_123/response', (string) $request->getUri()); - self::assertSame('application/json', $request->getHeaderLine('Content-Type')); - self::assertStringContainsString('"accept":true', (string) $request->getBody()); - } - - public function testListTeamRolesUsesExpectedEndpoint(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], '[]')); - $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + expect($result['ok'])->toBeTrue(); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('POST'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations/invites/inv_123/response'); + expect($request->getHeaderLine('Content-Type'))->toBe('application/json'); + expect((string) $request->getBody())->toContain('"accept":true'); + }); + test('list team roles uses expected endpoint', function () { + mockResponse('[]', 200); + $organizations = test()->facturapi->organizations(); $result = $organizations->listTeamRoles('org_123'); - self::assertIsArray($result); - $request = $httpClient->requests()[0]; - self::assertSame('GET', $request->getMethod()); - self::assertSame('https://www.facturapi.io/v2/organizations/org_123/team/roles', (string) $request->getUri()); - } - - public function testListTeamRoleTemplatesUsesExpectedEndpoint(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], '[]')); - $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + expect($result)->toBeArray(); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations/org_123/team/roles'); + }); + test('list team role templates uses expected endpoint', function () { + mockResponse('[]', 200); + $organizations = test()->facturapi->organizations(); $result = $organizations->listTeamRoleTemplates('org_123'); - self::assertIsArray($result); - $request = $httpClient->requests()[0]; - self::assertSame('GET', $request->getMethod()); - self::assertSame('https://www.facturapi.io/v2/organizations/org_123/team/roles/templates', (string) $request->getUri()); - } - - public function testListTeamRoleOperationsUsesExpectedEndpoint(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], '[]')); - $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + expect($result)->toBeArray(); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations/org_123/team/roles/templates'); + }); + test('list team role operations uses expected endpoint', function () { + mockResponse('[]', 200); + $organizations = test()->facturapi->organizations(); $result = $organizations->listTeamRoleOperations('org_123'); - self::assertIsArray($result); - $request = $httpClient->requests()[0]; - self::assertSame('GET', $request->getMethod()); - self::assertSame('https://www.facturapi.io/v2/organizations/org_123/team/roles/operations', (string) $request->getUri()); - } - - public function testRetrieveTeamRoleUsesExpectedEndpoint(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], '{"id":"role_123"}')); - $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + expect($result)->toBeArray(); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations/org_123/team/roles/operations'); + }); + test('retrieve team role uses expected endpoint', function () { + mockResponse('{"id":"role_123"}', 200); + $organizations = test()->facturapi->organizations(); $result = $organizations->retrieveTeamRole('org_123', 'role_123'); - self::assertSame('role_123', $result->id); - $request = $httpClient->requests()[0]; - self::assertSame('GET', $request->getMethod()); - self::assertSame('https://www.facturapi.io/v2/organizations/org_123/team/roles/role_123', (string) $request->getUri()); - } - - public function testCreateTeamRoleUsesExpectedEndpointAndJsonBody(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], '{"id":"role_123","name":"Billing analyst"}')); - $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + expect($result['id'])->toBe('role_123'); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations/org_123/team/roles/role_123'); + }); + test('create team role uses expected endpoint and json body', function () { + mockResponse('{"id":"role_123","name":"Billing analyst"}', 200); + $organizations = test()->facturapi->organizations(); $result = $organizations->createTeamRole('org_123', [ 'name' => 'Billing analyst', 'operations' => ['read:invoices'], ]); - self::assertSame('role_123', $result->id); - $request = $httpClient->requests()[0]; - self::assertSame('POST', $request->getMethod()); - self::assertSame('https://www.facturapi.io/v2/organizations/org_123/team/roles', (string) $request->getUri()); - self::assertSame('application/json', $request->getHeaderLine('Content-Type')); - self::assertStringContainsString('"name":"Billing analyst"', (string) $request->getBody()); - self::assertStringContainsString('"operations":["read:invoices"]', (string) $request->getBody()); - } - - public function testUpdateTeamAccessRoleUsesExpectedEndpointAndJsonBody(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], '{"id":"acc_123","role":"role_123"}')); - $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + expect($result['id'])->toBe('role_123'); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('POST'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations/org_123/team/roles'); + expect($request->getHeaderLine('Content-Type'))->toBe('application/json'); + expect((string) $request->getBody())->toContain('"name":"Billing analyst"'); + expect((string) $request->getBody())->toContain('"operations":["read:invoices"]'); + }); + test('update team access role uses expected endpoint and json body', function () { + mockResponse('{"id":"acc_123","role":"role_123"}', 200); + $organizations = test()->facturapi->organizations(); $result = $organizations->updateTeamAccessRole('org_123', 'acc_123', 'role_123'); - self::assertSame('acc_123', $result->id); - self::assertSame('role_123', $result->role); - $request = $httpClient->requests()[0]; - self::assertSame('PUT', $request->getMethod()); - self::assertSame('https://www.facturapi.io/v2/organizations/org_123/team/acc_123/role', (string) $request->getUri()); - self::assertSame('application/json', $request->getHeaderLine('Content-Type')); - self::assertStringContainsString('"role":"role_123"', (string) $request->getBody()); - } - - public function testRemoveTeamAccessUsesExpectedEndpoint(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], '{"ok":true}')); - $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + expect($result['id'])->toBe('acc_123'); + expect($result['role'])->toBe('role_123'); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('PUT'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations/org_123/team/acc_123/role'); + expect($request->getHeaderLine('Content-Type'))->toBe('application/json'); + expect((string) $request->getBody())->toContain('"role":"role_123"'); + }); + test('remove team access uses expected endpoint', function () { + mockResponse('{"ok":true}', 200); + $organizations = test()->facturapi->organizations(); $result = $organizations->removeTeamAccess('org_123', 'acc_123'); - self::assertTrue($result->ok); - $request = $httpClient->requests()[0]; - self::assertSame('DELETE', $request->getMethod()); - self::assertSame('https://www.facturapi.io/v2/organizations/org_123/team/acc_123', (string) $request->getUri()); - } - - public function testUpdateTeamRoleUsesExpectedEndpointAndJsonBody(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], '{"id":"role_123","name":"Senior billing analyst"}')); - $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + expect($result['ok'])->toBeTrue(); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('DELETE'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations/org_123/team/acc_123'); + }); + test('update team role uses expected endpoint and json body', function () { + mockResponse('{"id":"role_123","name":"Senior billing analyst"}', 200); + $organizations = test()->facturapi->organizations(); $result = $organizations->updateTeamRole('org_123', 'role_123', [ 'name' => 'Senior billing analyst', ]); - self::assertSame('role_123', $result->id); - self::assertSame('Senior billing analyst', $result->name); - $request = $httpClient->requests()[0]; - self::assertSame('PUT', $request->getMethod()); - self::assertSame('https://www.facturapi.io/v2/organizations/org_123/team/roles/role_123', (string) $request->getUri()); - self::assertSame('application/json', $request->getHeaderLine('Content-Type')); - self::assertStringContainsString('"name":"Senior billing analyst"', (string) $request->getBody()); - } - - public function testDeleteTeamRoleUsesExpectedEndpoint(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], '{"ok":true}')); - $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + expect($result['id'])->toBe('role_123'); + expect($result['name'])->toBe('Senior billing analyst'); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('PUT'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations/org_123/team/roles/role_123'); + expect($request->getHeaderLine('Content-Type'))->toBe('application/json'); + expect((string) $request->getBody())->toContain('"name":"Senior billing analyst"'); + }); + test('delete team role uses expected endpoint', function () { + mockResponse('{"ok":true}', 200); + $organizations = test()->facturapi->organizations(); $result = $organizations->deleteTeamRole('org_123', 'role_123'); - self::assertTrue($result->ok); - $request = $httpClient->requests()[0]; - self::assertSame('DELETE', $request->getMethod()); - self::assertSame('https://www.facturapi.io/v2/organizations/org_123/team/roles/role_123', (string) $request->getUri()); - } -} + expect($result['ok'])->toBeTrue(); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('DELETE'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations/org_123/team/roles/role_123'); + }); + +}); diff --git a/tests/Resources/OrganizationsTest.php b/tests/Resources/OrganizationsTest.php new file mode 100644 index 0000000..bcf1e08 --- /dev/null +++ b/tests/Resources/OrganizationsTest.php @@ -0,0 +1,316 @@ +facturapi->organizations(); + + $organizations->all(); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations'); + }); + + test('retrieve fetches a single organization by id', function () { + mockResponse('{"id":"org_123"}', 200); + $organizations = test()->facturapi->organizations(); + + $result = $organizations->retrieve('org_123'); + + expect($result->id)->toBe('org_123'); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations/org_123'); + }); + + test('create sends json body with name', function () { + mockResponse('{"id":"org_123"}', 201); + $organizations = test()->facturapi->organizations(); + + $payload = ['name' => 'Mi Empresa']; + $result = $organizations->create($payload); + + expect($result->id)->toBe('org_123'); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('POST'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations'); + expect(json_decode((string) $request->getBody(), true))->toBe($payload); + }); + + test('updateLegal updates legal info', function () { + mockResponse(json_encode([ + 'id' => 'org_123', + 'name' => 'Mi Empresa', + 'legal' => [ + 'legal_name' => 'Empresa SA', + 'tax_id' => 'XAXX010101000', + 'tax_system' => '601', + 'website' => 'https://empresa.example.com', + 'support_email' => 'support@empresa.example.com', + 'phone' => '5512345678', + 'address' => [ + 'zip' => '01000', + 'street' => 'Av. Insurgentes Sur', + 'exterior' => '100', + ], + ], + ]), 200); + $organizations = test()->facturapi->organizations(); + + $payload = [ + 'name' => 'Mi Empresa', + 'legal_name' => 'Empresa SA', + 'tax_id' => 'XAXX010101000', + 'tax_system' => '601', + 'website' => 'https://empresa.example.com', + 'support_email' => 'support@empresa.example.com', + 'phone' => '5512345678', + 'address' => [ + 'zip' => '01000', + 'street' => 'Av. Insurgentes Sur', + 'exterior' => '100', + ], + ]; + $result = $organizations->updateLegal('org_123', $payload); + + expect($result->id)->toBe('org_123'); + expect($result->name)->toBe('Mi Empresa'); + expect($result->legal['legal_name'])->toBe('Empresa SA'); + expect($result->legal['tax_id'])->toBe('XAXX010101000'); + expect($result->legal['tax_system'])->toBe('601'); + expect($result->legal['website'])->toBe('https://empresa.example.com'); + expect($result->legal['support_email'])->toBe('support@empresa.example.com'); + expect($result->legal['phone'])->toBe('5512345678'); + expect($result->legal['address']['zip'])->toBe('01000'); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('PUT'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations/org_123/legal'); + expect(json_decode((string) $request->getBody(), true))->toBe($payload); + }); + + test('updateCustomization updates colors', function () { + mockResponse(json_encode([ + 'id' => 'org_123', + 'customization' => [ + 'color' => '#FFFFFF', + 'next_folio_number' => 10, + 'next_folio_number_test' => 5, + 'pdf_extra' => 'Custom footer text', + ], + ]), 200); + $organizations = test()->facturapi->organizations(); + + $payload = [ + 'color' => '#FFFFFF', + 'next_folio_number' => 10, + 'next_folio_number_test' => 5, + 'pdf_extra' => 'Custom footer text', + ]; + $result = $organizations->updateCustomization('org_123', $payload); + + expect($result->id)->toBe('org_123'); + expect($result->customization['color'])->toBe('#FFFFFF'); + expect($result->customization['next_folio_number'])->toBe(10); + expect($result->customization['next_folio_number_test'])->toBe(5); + expect($result->customization['pdf_extra'])->toBe('Custom footer text'); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('PUT'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations/org_123/customization'); + expect(json_decode((string) $request->getBody(), true))->toBe($payload); + }); + + test('updateReceiptSettings updates settings', function () { + mockResponse(json_encode([ + 'id' => 'org_123', + 'receipts' => [ + 'next_folio_number' => 100, + 'next_folio_number_test' => 50, + 'periodicity' => 'monthly', + 'duration_days' => 30, + 'activate_global_invoice' => true, + 'grouped_items_invoice' => false, + ], + ]), 200); + $organizations = test()->facturapi->organizations(); + + $payload = [ + 'next_folio_number' => 100, + 'next_folio_number_test' => 50, + 'periodicity' => 'monthly', + 'duration_days' => 30, + 'activate_global_invoice' => true, + 'grouped_items_invoice' => false, + ]; + $result = $organizations->updateReceiptSettings('org_123', $payload); + + expect($result->id)->toBe('org_123'); + expect($result->receipts['next_folio_number'])->toBe(100); + expect($result->receipts['next_folio_number_test'])->toBe(50); + expect($result->receipts['periodicity'])->toBe('monthly'); + expect($result->receipts['duration_days'])->toBe(30); + expect($result->receipts['activate_global_invoice'])->toBeTrue(); + expect($result->receipts['grouped_items_invoice'])->toBeFalse(); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('PUT'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations/org_123/receipts'); + expect(json_decode((string) $request->getBody(), true))->toBe($payload); + }); + + test('updateDomain updates domain', function () { + mockResponse('{"id":"org_123"}', 200); + $organizations = test()->facturapi->organizations(); + + $payload = ['domain' => 'my-domain']; + $result = $organizations->updateDomain('org_123', $payload); + + expect($result->id)->toBe('org_123'); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('PUT'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations/org_123/domain'); + expect(json_decode((string) $request->getBody(), true))->toBe($payload); + }); + + test('updateSelfInvoiceSettings updates self invoice settings', function () { + mockResponse('{"id":"org_123"}', 200); + $organizations = test()->facturapi->organizations(); + + $payload = ['active' => true]; + $result = $organizations->updateSelfInvoiceSettings('org_123', $payload); + + expect($result->id)->toBe('org_123'); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('PUT'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations/org_123/self-invoice'); + expect(json_decode((string) $request->getBody(), true))->toBe($payload); + }); + + test('getTestApiKey retrieves test key', function () { + mockResponse('{"id":"org_123"}', 200); + $organizations = test()->facturapi->organizations(); + + $result = $organizations->getTestApiKey('org_123'); + + expect($result['id'])->toBe('org_123'); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations/org_123/apikeys/test'); + }); + + test('renewTestApiKey renews test key', function () { + mockResponse('{"id":"org_123"}', 200); + $organizations = test()->facturapi->organizations(); + + $result = $organizations->renewTestApiKey('org_123'); + + expect($result['id'])->toBe('org_123'); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('PUT'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations/org_123/apikeys/test'); + }); + + test('listLiveApiKeys retrieves live keys', function () { + mockResponse('{"data":[]}', 200); + $organizations = test()->facturapi->organizations(); + + $organizations->listLiveApiKeys('org_123'); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations/org_123/apikeys/live'); + }); + + test('createSeriesGroup creates series group for organization', function () { + mockResponse(json_encode([ + 'series' => 'B', + 'next_folio' => 1, + 'next_folio_test' => 1, + ]), 201); + $organizations = test()->facturapi->organizations(); + + $payload = [ + 'series' => 'B', + 'next_folio' => 1, + 'next_folio_test' => 1, + ]; + $result = $organizations->createSeriesGroup('org_123', $payload); + + expect($result['series'])->toBe('B'); + expect($result['next_folio'])->toBe(1); + expect($result['next_folio_test'])->toBe(1); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('POST'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations/org_123/series-group'); + expect(json_decode((string) $request->getBody(), true))->toBe($payload); + }); + + test('updateSeriesGroup updates series group for organization', function () { + mockResponse(json_encode([ + 'series' => 'B', + 'next_folio' => 10, + 'next_folio_test' => 5, + ]), 200); + $organizations = test()->facturapi->organizations(); + + $payload = [ + 'next_folio' => 10, + 'next_folio_test' => 5, + ]; + $result = $organizations->updateSeriesGroup('org_123', 'B', $payload); + + expect($result['series'])->toBe('B'); + expect($result['next_folio'])->toBe(10); + expect($result['next_folio_test'])->toBe(5); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('PUT'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations/org_123/series-group/B'); + expect(json_decode((string) $request->getBody(), true))->toBe($payload); + }); + + test('updateDefaultSeries updates default series for organization', function () { + mockResponse(json_encode([ + 'type' => 'invoice', + 'series' => 'B', + ]), 200); + $organizations = test()->facturapi->organizations(); + + $payload = [ + 'type' => 'invoice', + 'series' => 'B', + ]; + $result = $organizations->updateDefaultSeries('org_123', $payload); + + expect($result['type'])->toBe('invoice'); + expect($result['series'])->toBe('B'); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('PUT'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations/org_123/series-group/default-series'); + expect(json_decode((string) $request->getBody(), true))->toBe($payload); + }); + + test('deleteSeriesGroup deletes series group for organization', function () { + mockResponse(json_encode([ + 'series' => 'B', + 'next_folio' => 1, + 'next_folio_test' => 1, + ]), 200); + $organizations = test()->facturapi->organizations(); + + $result = $organizations->deleteSeriesGroup('org_123', 'B'); + + expect($result['series'])->toBe('B'); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('DELETE'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/organizations/org_123/series-group/B'); + }); +}); diff --git a/tests/Resources/ProductsTest.php b/tests/Resources/ProductsTest.php new file mode 100644 index 0000000..c15dcf8 --- /dev/null +++ b/tests/Resources/ProductsTest.php @@ -0,0 +1,502 @@ + 'prod_123', + 'created_at' => '2025-01-15T10:30:00.000Z', + 'livemode' => false, + 'description' => 'Guitarra electroacústica Fender', + 'product_key' => '60131324', + 'price' => 12500.50, + 'tax_included' => true, + 'taxability' => '02', + 'taxes' => [ + [ + 'type' => 'IVA', + 'rate' => 0.16, + 'base' => 12500.50, + 'factor' => 'Tasa', + 'withholding' => false, + ], + [ + 'type' => 'IEPS', + 'rate' => 0.08, + 'base' => 12500.50, + 'factor' => 'Tasa', + 'withholding' => false, + 'ieps_mode' => 'sum_before_taxes', + ], + ], + 'local_taxes' => [ + [ + 'type' => 'ISH', + 'rate' => 0.03, + 'base' => 12500.50, + 'withholding' => false, + ], + ], + 'unit_key' => 'H87', + 'unit_name' => 'Pieza', + 'sku' => 'FEN-GTR-001', + ]; + + mockResponse((string) json_encode($responseBody), 201); + $products = test()->facturapi->products(); + + $payload = [ + 'description' => 'Guitarra electroacústica Fender', + 'product_key' => '60131324', + 'price' => 12500.50, + 'tax_included' => true, + 'taxability' => '02', + 'taxes' => [ + [ + 'type' => 'IVA', + 'rate' => 0.16, + 'base' => 12500.50, + 'factor' => 'Tasa', + 'withholding' => false, + ], + [ + 'type' => 'IEPS', + 'rate' => 0.08, + 'base' => 12500.50, + 'factor' => 'Tasa', + 'withholding' => false, + 'ieps_mode' => 'sum_before_taxes', + ], + ], + 'local_taxes' => [ + [ + 'type' => 'ISH', + 'rate' => 0.03, + 'base' => 12500.50, + 'withholding' => false, + ], + ], + 'unit_key' => 'H87', + 'unit_name' => 'Pieza', + 'sku' => 'FEN-GTR-001', + ]; + + $result = $products->create($payload); + + expect($result)->toBeInstanceOf(Product::class); + expect($result->id)->toBe('prod_123'); + expect($result->created_at)->toBe('2025-01-15T10:30:00.000Z'); + expect($result->livemode)->toBeFalse(); + expect($result->description)->toBe('Guitarra electroacústica Fender'); + expect($result->product_key)->toBe('60131324'); + expect($result->price)->toBe(12500.50); + expect($result->tax_included)->toBeTrue(); + expect($result->taxability)->toBe('02'); + expect($result->unit_key)->toBe('H87'); + expect($result->unit_name)->toBe('Pieza'); + expect($result->sku)->toBe('FEN-GTR-001'); + + expect($result->taxes)->toBeArray()->toHaveCount(2); + expect($result->taxes[0])->toBeInstanceOf(BaseTax::class); + expect($result->taxes[0]->type)->toBe('IVA'); + expect($result->taxes[0]->rate)->toBe(0.16); + expect($result->taxes[0]->base)->toBe(12500.50); + expect($result->taxes[0]->factor)->toBe('Tasa'); + expect($result->taxes[0]->withholding)->toBeFalse(); + + expect($result->taxes[1])->toBeInstanceOf(BaseTax::class); + expect($result->taxes[1])->toBeInstanceOf(IepsTax::class); + expect($result->taxes[1]->type)->toBe('IEPS'); + expect($result->taxes[1]->rate)->toBe(0.08); + expect($result->taxes[1]->base)->toBe(12500.50); + expect($result->taxes[1]->factor)->toBe('Tasa'); + expect($result->taxes[1]->withholding)->toBeFalse(); + expect($result->taxes[1]->ieps_mode)->toBe('sum_before_taxes'); + + expect($result->local_taxes)->toBeArray()->toHaveCount(1); + expect($result->local_taxes[0])->toBeInstanceOf(LocalTax::class); + expect($result->local_taxes[0]->type)->toBe('ISH'); + expect($result->local_taxes[0]->rate)->toBe(0.03); + expect($result->local_taxes[0]->base)->toBe(12500.50); + expect($result->local_taxes[0]->withholding)->toBeFalse(); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('POST'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/products'); + expect($request->getHeaderLine('Content-Type'))->toBe('application/json'); + + $sentPayload = json_decode((string) $request->getBody(), true); + expect($sentPayload)->toBe($payload); + }); + + test('create sends json body with minimal required fields', function () { + $responseBody = [ + 'id' => 'prod_min_123', + 'created_at' => '2025-01-15T11:00:00.000Z', + 'livemode' => false, + 'description' => 'Servicio de desarrollo', + 'product_key' => '84111506', + 'price' => 5000.50, + 'unit_key' => 'E48', + ]; + + mockResponse((string) json_encode($responseBody), 201); + $products = test()->facturapi->products(); + + $payload = [ + 'description' => 'Servicio de desarrollo', + 'product_key' => '84111506', + 'price' => 5000.50, + 'unit_key' => 'E48', + ]; + + $result = $products->create($payload); + + expect($result)->toBeInstanceOf(Product::class); + expect($result->id)->toBe('prod_min_123'); + expect($result->description)->toBe('Servicio de desarrollo'); + expect($result->product_key)->toBe('84111506'); + expect($result->price)->toBe(5000.50); + expect($result->unit_key)->toBe('E48'); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('POST'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/products'); + expect($request->getHeaderLine('Content-Type'))->toBe('application/json'); + + $sentPayload = json_decode((string) $request->getBody(), true); + expect($sentPayload)->toBe($payload); + }); + + test('retrieve fetches a single product by id', function () { + $responseBody = [ + 'id' => 'prod_123', + 'created_at' => '2025-01-15T10:30:00.000Z', + 'livemode' => false, + 'description' => 'Guitarra electroacústica Fender', + 'product_key' => '60131324', + 'price' => 12500.50, + 'tax_included' => true, + 'taxability' => '02', + 'taxes' => [ + [ + 'type' => 'IVA', + 'rate' => 0.16, + 'base' => 12500.50, + 'factor' => 'Tasa', + 'withholding' => false, + ], + ], + 'local_taxes' => [], + 'unit_key' => 'H87', + 'unit_name' => 'Pieza', + 'sku' => 'FEN-GTR-001', + ]; + + mockResponse((string) json_encode($responseBody), 200); + $products = test()->facturapi->products(); + + $result = $products->retrieve('prod_123'); + + expect($result)->toBeInstanceOf(Product::class); + expect($result->id)->toBe('prod_123'); + expect($result->created_at)->toBe('2025-01-15T10:30:00.000Z'); + expect($result->livemode)->toBeFalse(); + expect($result->description)->toBe('Guitarra electroacústica Fender'); + expect($result->product_key)->toBe('60131324'); + expect($result->price)->toBe(12500.50); + expect($result->tax_included)->toBeTrue(); + expect($result->taxability)->toBe('02'); + expect($result->unit_key)->toBe('H87'); + expect($result->unit_name)->toBe('Pieza'); + expect($result->sku)->toBe('FEN-GTR-001'); + expect($result->taxes)->toBeArray()->toHaveCount(1); + expect($result->taxes[0]->type)->toBe('IVA'); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/products/prod_123'); + }); + + test('update sends put request with all properties', function () { + $responseBody = [ + 'id' => 'prod_123', + 'created_at' => '2025-01-15T10:30:00.000Z', + 'livemode' => false, + 'description' => 'Guitarra Fender Stratocaster Custom', + 'product_key' => '60131324', + 'price' => 18999.99, + 'tax_included' => false, + 'taxability' => '02', + 'taxes' => [ + [ + 'type' => 'IVA', + 'rate' => 0.16, + 'base' => 18999.99, + 'factor' => 'Tasa', + 'withholding' => false, + ], + [ + 'type' => 'ISR', + 'rate' => 0.10, + 'base' => 18999.99, + 'factor' => 'Tasa', + 'withholding' => true, + ], + ], + 'local_taxes' => [ + [ + 'type' => 'ISH', + 'rate' => 0.04, + 'base' => 18999.99, + 'withholding' => false, + ], + ], + 'unit_key' => 'H87', + 'unit_name' => 'Pieza', + 'sku' => 'FEN-GTR-002', + ]; + + mockResponse((string) json_encode($responseBody), 200); + $products = test()->facturapi->products(); + + $payload = [ + 'description' => 'Guitarra Fender Stratocaster Custom', + 'product_key' => '60131324', + 'price' => 18999.99, + 'tax_included' => false, + 'taxability' => '02', + 'taxes' => [ + [ + 'type' => 'IVA', + 'rate' => 0.16, + 'base' => 18999.99, + 'factor' => 'Tasa', + 'withholding' => false, + ], + [ + 'type' => 'ISR', + 'rate' => 0.10, + 'base' => 18999.99, + 'factor' => 'Tasa', + 'withholding' => true, + ], + ], + 'local_taxes' => [ + [ + 'type' => 'ISH', + 'rate' => 0.04, + 'base' => 18999.99, + 'withholding' => false, + ], + ], + 'unit_key' => 'H87', + 'unit_name' => 'Pieza', + 'sku' => 'FEN-GTR-002', + ]; + + $result = $products->update('prod_123', $payload); + + expect($result)->toBeInstanceOf(Product::class); + expect($result->id)->toBe('prod_123'); + expect($result->description)->toBe('Guitarra Fender Stratocaster Custom'); + expect($result->price)->toBe(18999.99); + expect($result->tax_included)->toBeFalse(); + expect($result->sku)->toBe('FEN-GTR-002'); + expect($result->taxes)->toBeArray()->toHaveCount(2); + expect($result->taxes[0]->type)->toBe('IVA'); + expect($result->taxes[1]->type)->toBe('ISR'); + expect($result->taxes[1]->withholding)->toBeTrue(); + expect($result->local_taxes)->toBeArray()->toHaveCount(1); + expect($result->local_taxes[0]->rate)->toBe(0.04); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('PUT'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/products/prod_123'); + expect($request->getHeaderLine('Content-Type'))->toBe('application/json'); + + $sentPayload = json_decode((string) $request->getBody(), true); + expect($sentPayload)->toBe($payload); + }); + + test('update sends partial payload for editing specific product attributes', function () { + $responseBody = [ + 'id' => 'prod_123', + 'created_at' => '2025-01-15T10:30:00.000Z', + 'livemode' => false, + 'description' => 'Guitarra Fender Stratocaster Edición 2025', + 'product_key' => '60131324', + 'price' => 19500.50, + 'unit_key' => 'H87', + ]; + + mockResponse((string) json_encode($responseBody), 200); + $products = test()->facturapi->products(); + + $payload = [ + 'description' => 'Guitarra Fender Stratocaster Edición 2025', + 'price' => 19500.50, + ]; + + $result = $products->update('prod_123', $payload); + + expect($result)->toBeInstanceOf(Product::class); + expect($result->id)->toBe('prod_123'); + expect($result->description)->toBe('Guitarra Fender Stratocaster Edición 2025'); + expect($result->price)->toBe(19500.50); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('PUT'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/products/prod_123'); + expect($request->getHeaderLine('Content-Type'))->toBe('application/json'); + + $sentPayload = json_decode((string) $request->getBody(), true); + expect($sentPayload)->toBe($payload); + }); + + test('delete removes a product by id', function () { + $responseBody = [ + 'id' => 'prod_123', + 'created_at' => '2025-01-15T10:30:00.000Z', + 'livemode' => false, + 'description' => 'Guitarra electroacústica Fender', + 'product_key' => '60131324', + 'price' => 12500.50, + 'unit_key' => 'H87', + ]; + + mockResponse((string) json_encode($responseBody), 200); + $products = test()->facturapi->products(); + + $result = $products->delete('prod_123'); + + expect($result)->toBeInstanceOf(Product::class); + expect($result->id)->toBe('prod_123'); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('DELETE'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/products/prod_123'); + }); + + test('all retrieves paginated list of products without parameters', function () { + $responseBody = [ + 'page' => 1, + 'total_pages' => 1, + 'total_results' => 1, + 'data' => [ + [ + 'id' => 'prod_1', + 'created_at' => '2025-01-01T00:00:00.000Z', + 'livemode' => false, + 'description' => 'Producto 1', + 'product_key' => '01010101', + 'price' => 100.0, + 'unit_key' => 'H87', + ], + ], + ]; + + mockResponse((string) json_encode($responseBody), 200); + $products = test()->facturapi->products(); + + $result = $products->all(); + + expect($result)->toBeInstanceOf(ProductSearchResult::class); + expect($result->page)->toBe(1); + expect($result->total_pages)->toBe(1); + expect($result->total_results)->toBe(1); + expect($result->data)->toBeArray()->toHaveCount(1); + expect($result->data[0])->toBeInstanceOf(Product::class); + expect($result->data[0]->id)->toBe('prod_1'); + expect($result->data[0]->description)->toBe('Producto 1'); + expect($result->data[0]->price)->toBe(100.0); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/products'); + }); + + test('all serializes query filters for search, sku, pagination, and date ranges', function () { + mockResponse('{"page":2,"total_pages":5,"total_results":120,"data":[]}', 200); + $products = test()->facturapi->products(); + + $filters = [ + 'q' => 'laptop', + 'sku' => 'SKU-LAP-001', + 'page' => 2, + 'limit' => 25, + 'date' => [ + 'gt' => '2025-01-01', + 'gte' => '2025-01-02', + 'lt' => '2025-12-31', + 'lte' => '2025-12-30', + ], + ]; + + $result = $products->all($filters); + + expect($result)->toBeInstanceOf(ProductSearchResult::class); + expect($result->page)->toBe(2); + expect($result->total_pages)->toBe(5); + expect($result->total_results)->toBe(120); + expect($result->data)->toBeArray()->toBeEmpty(); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect(urldecode($request->getUri()->getQuery()))->toBe('q=laptop&sku=SKU-LAP-001&page=2&limit=25&date[gt]=2025-01-01&date[gte]=2025-01-02&date[lt]=2025-12-31&date[lte]=2025-12-30'); + }); + + test('retrieve throws NotFoundException when product is not found', function () { + $errorBody = [ + 'message' => 'Product not found', + ]; + + mockResponse((string) json_encode($errorBody), 404, ['Content-Type' => 'application/json']); + $products = test()->facturapi->products(); + + expect(fn () => $products->retrieve('prod_nonexistent')) + ->toThrow(function (NotFoundException $exception) { + expect($exception->getStatusCode())->toBe(404); + expect($exception->getMessage())->toBe('Product not found'); + }); + }); + + test('create throws ValidationException on invalid request payload', function () { + $errorBody = [ + 'message' => 'Validation Error: price is required', + ]; + + mockResponse((string) json_encode($errorBody), 400, ['Content-Type' => 'application/json']); + $products = test()->facturapi->products(); + + expect(fn () => $products->create(['description' => 'Incomplete product'])) + ->toThrow(function (ValidationException $exception) { + expect($exception->getStatusCode())->toBe(400); + expect($exception->getMessage())->toBe('Validation Error: price is required'); + }); + }); + + test('delete throws NotFoundException when deleting non-existent product', function () { + $errorBody = [ + 'message' => 'Product not found', + ]; + + mockResponse((string) json_encode($errorBody), 404, ['Content-Type' => 'application/json']); + $products = test()->facturapi->products(); + + expect(fn () => $products->delete('prod_nonexistent')) + ->toThrow(function (NotFoundException $exception) { + expect($exception->getStatusCode())->toBe(404); + expect($exception->getMessage())->toBe('Product not found'); + }); + }); +}); diff --git a/tests/Resources/ReceiptsTest.php b/tests/Resources/ReceiptsTest.php new file mode 100644 index 0000000..98a50b4 --- /dev/null +++ b/tests/Resources/ReceiptsTest.php @@ -0,0 +1,220 @@ +facturapi->receipts(); + + $payload = [ + 'customer' => 'cus_123', + 'address' => [ + 'zip' => '01000', + 'street' => 'Av. Insurgentes Sur' + ], + 'items' => [ + [ + 'quantity' => 1, + 'product' => [ + 'description' => 'Servicio', + 'price' => 100.5, + 'product_key' => '81111509' + ] + ] + ], + 'date' => '2025-03-01T12:00:00Z', + 'payment_form' => '03', + 'folio_number' => '12345', + 'currency' => 'USD', + 'exchange' => 20.5, + 'branch' => 'Principal', + 'external_id' => 'ext_001', + 'idempotency_key' => 'idemp_abc123' + ]; + + $result = $receipts->create($payload); + + expect($result->id)->toBe('rec_123'); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('POST'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/receipts'); + + $sentPayload = json_decode((string) $request->getBody(), true); + expect($sentPayload)->toBe($payload); + }); + + test('all serializes query parameters correctly', function () { + mockResponse('{"data":[]}', 200); + $receipts = test()->facturapi->receipts(); + + $receipts->all([ + 'q' => 'recibo', + 'customer' => 'cus_123', + 'payment_form' => '01', + 'date' => [ + 'gte' => '2023-01-01', + 'lte' => '2023-12-31' + ], + 'invoice' => 'inv_123', + 'page' => 2, + 'limit' => 20 + ]); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect(urldecode($request->getUri()->getQuery()))->toBe('q=recibo&customer=cus_123&payment_form=01&date[gte]=2023-01-01&date[lte]=2023-12-31&invoice=inv_123&page=2&limit=20'); + }); + + test('retrieve fetches a single receipt', function () { + mockResponse('{"id":"rec_123"}', 200); + $receipts = test()->facturapi->receipts(); + + $result = $receipts->retrieve('rec_123'); + + expect($result->id)->toBe('rec_123'); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/receipts/rec_123'); + }); + + test('invoice creates an invoice for a receipt', function () { + mockResponse('{"id":"inv_123"}', 201); + $receipts = test()->facturapi->receipts(); + + $payload = ['use' => 'G03']; + $result = $receipts->invoice('rec_123', $payload); + + expect($result->id)->toBe('inv_123'); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('POST'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/receipts/rec_123/invoice'); + expect(json_decode((string) $request->getBody(), true))->toBe($payload); + }); + + test('createGlobalInvoice uses expected endpoint', function () { + mockResponse('{"id":"inv_123"}', 201); + $receipts = test()->facturapi->receipts(); + + $payload = ['periodicity' => '04', 'months' => '03', 'year' => 2025]; + $result = $receipts->createGlobalInvoice($payload); + + expect($result->id)->toBe('inv_123'); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('POST'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/receipts/global-invoice'); + }); + + test('toInvoice creates an invoice from multiple receipts', function () { + mockResponse('{"id":"inv_123"}', 201); + $receipts = test()->facturapi->receipts(); + + $payload = ['keys' => ['rec_1', 'rec_2'], 'customer' => 'cus_123']; + $result = $receipts->toInvoice($payload); + + expect($result->id)->toBe('inv_123'); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('POST'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/receipts/to-invoice'); + expect(json_decode((string) $request->getBody(), true))->toBe($payload); + }); + + test('previewToInvoicePdf generates a pdf from multiple receipts', function () { + mockResponse('PDF_BYTES', 200); + $receipts = test()->facturapi->receipts(); + + $payload = ['keys' => ['rec_1', 'rec_2']]; + $result = $receipts->previewToInvoicePdf($payload); + + expect($result)->toBe('PDF_BYTES'); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('POST'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/receipts/to-invoice/preview'); + expect(json_decode((string) $request->getBody(), true))->toBe($payload); + }); + + test('previewToInvoiceDownloadUrl obtains signed download url using keys', function () { + mockResponse(json_encode([ + 'url' => 'https://facturapi-signed.s3.amazonaws.com/preview.pdf', + 'expires_at' => '2025-03-01T12:00:00Z', + 'content_type' => 'application/pdf', + 'filename' => 'preview.pdf', + ]), 200); + $receipts = test()->facturapi->receipts(); + + $payload = ['keys' => ['rec_1', 'rec_2']]; + $result = $receipts->previewToInvoiceDownloadUrl($payload); + + expect($result->url)->toBe('https://facturapi-signed.s3.amazonaws.com/preview.pdf'); + expect($result->filename)->toBe('preview.pdf'); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('POST'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/receipts/to-invoice/preview/download-url'); + expect(json_decode((string) $request->getBody(), true))->toBe($payload); + }); + + test('cancel removes a receipt', function () { + mockResponse('{"id":"rec_123"}', 200); + $receipts = test()->facturapi->receipts(); + + $result = $receipts->cancel('rec_123'); + + expect($result->id)->toBe('rec_123'); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('DELETE'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/receipts/rec_123'); + }); + + test('sendByEmail sends single email string to customer', function () { + mockResponse('{"success":true}', 200); + $receipts = test()->facturapi->receipts(); + + $result = $receipts->sendByEmail('rec_123', 'user@example.com'); + + expect($result->success)->toBeTrue(); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('POST'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/receipts/rec_123/email'); + expect(json_decode((string) $request->getBody(), true))->toBe(['email' => ['user@example.com']]); + }); + + test('sendByEmail sends array of emails to customer', function () { + mockResponse('{"success":true}', 200); + $receipts = test()->facturapi->receipts(); + + $result = $receipts->sendByEmail('rec_123', ['user1@example.com', 'user2@example.com']); + + expect($result->success)->toBeTrue(); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('POST'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/receipts/rec_123/email'); + expect(json_decode((string) $request->getBody(), true))->toBe(['email' => ['user1@example.com', 'user2@example.com']]); + }); + + test('sendByEmail sends empty payload when email is null', function () { + mockResponse('{"success":true}', 200); + $receipts = test()->facturapi->receipts(); + + $result = $receipts->sendByEmail('rec_123'); + + expect($result->success)->toBeTrue(); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('POST'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/receipts/rec_123/email'); + expect((string) $request->getBody())->toBe(''); + }); + + test('downloadPdf downloads pdf bytes', function () { + mockResponse('PDF_BYTES', 200); + $receipts = test()->facturapi->receipts(); + + $result = $receipts->downloadPdf('rec_123'); + + expect($result)->toBe('PDF_BYTES'); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/receipts/rec_123/pdf'); + }); +}); diff --git a/tests/Resources/RetentionsTest.php b/tests/Resources/RetentionsTest.php index ae1d6bf..f186866 100644 --- a/tests/Resources/RetentionsTest.php +++ b/tests/Resources/RetentionsTest.php @@ -1,102 +1,192 @@ $httpClient]); +describe('Retentions', function () { + test('create can send draft retention payload', function () { + mockResponse('{"id":"ret_draft","status":"draft"}', 200); + $retentions = test()->facturapi->retentions(); $retentions->create([ 'status' => 'draft', 'customer' => null, ]); - $request = $httpClient->requests()[0]; - self::assertSame('POST', $request->getMethod()); - self::assertSame('https://www.facturapi.io/v2/retentions', (string) $request->getUri()); - self::assertJsonStringEqualsJsonString( - '{"status":"draft","customer":null}', - (string) $request->getBody() - ); - } - - public function testUpdateDraftUsesPutOnRetentionId(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], '{"id":"ret_123"}')); - $retentions = new Retentions('sk_test_abc123', ['httpClient' => $httpClient]); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('POST'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/retentions'); + expect((string) $request->getBody())->toBeJson()->and(json_decode((string) $request->getBody(), true))->toBe(json_decode('{"status":"draft","customer":null}', true)); + }); + test('create can send full retention payload', function () { + mockResponse('{"id":"ret_123"}', 201); + $retentions = test()->facturapi->retentions(); + + $payload = [ + 'customer' => 'cus_123', + 'cve_retenc' => '01', + 'fecha_exp' => '2025-03-01T12:00:00Z', + 'desc_retenc' => 'Servicios profesionales', + 'folio_int' => '12345', + 'periodo' => [ + 'mes_ini' => '01', + 'mes_fin' => '01', + 'ejerc' => 2025 + ], + 'totales' => [ + 'monto_tot_operacion' => 1000.5, + 'monto_tot_grav' => 1000.5, + 'monto_tot_exent' => 0.5, + 'monto_tot_ret' => 100.5, + 'impuestos_retenidos' => [ + [ + 'base_ret' => 1000.5, + 'impuesto' => '001', + 'monto_ret' => 100.5, + 'tipo_pago_ret' => 'Pago definitivo' + ] + ] + ], + 'external_id' => 'ext_001', + 'idempotency_key' => 'idemp_ret', + 'pdf_custom_section' => 'Custom text', + 'addenda' => 'XML string', + 'namespaces' => [['namespace' => 'x', 'uri' => 'y']], + 'complements' => [ + [ + 'type' => 'dividendos', + 'data' => [ + 'cve_tip_div_ou_til' => '01', + 'monto_is_r_ret_div_ext' => 0, + 'tip_pago_ret_div_ext' => 'Pago provisional' + ] + ] + ] + ]; + + $retentions->create($payload); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('POST'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/retentions'); + expect(json_decode((string) $request->getBody(), true))->toBe($payload); + }); + + test('update draft uses put on retention id', function () { + mockResponse('{"id":"ret_123"}', 200); + $retentions = test()->facturapi->retentions(); $retentions->updateDraft('ret_123', [ 'folio_int' => 'R-2026-001', ]); - $request = $httpClient->requests()[0]; - self::assertSame('PUT', $request->getMethod()); - self::assertSame('https://www.facturapi.io/v2/retentions/ret_123', (string) $request->getUri()); - self::assertJsonStringEqualsJsonString( - '{"folio_int":"R-2026-001"}', - (string) $request->getBody() - ); - } + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('PUT'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/retentions/ret_123'); + expect((string) $request->getBody())->toBeJson()->and(json_decode((string) $request->getBody(), true))->toBe(['folio_int' => 'R-2026-001']); + }); - public function testCopyToDraftUsesCopyEndpoint(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], '{"id":"ret_draft"}')); - $retentions = new Retentions('sk_test_abc123', ['httpClient' => $httpClient]); + test('retrieve fetches a single retention', function () { + mockResponse('{"id":"ret_123"}', 200); + $retentions = test()->facturapi->retentions(); - $retentions->copyToDraft('ret_123'); + $result = $retentions->retrieve('ret_123'); - $request = $httpClient->requests()[0]; - self::assertSame('POST', $request->getMethod()); - self::assertSame('https://www.facturapi.io/v2/retentions/ret_123/copy', (string) $request->getUri()); - self::assertSame('', (string) $request->getBody()); - } + expect($result->id)->toBe('ret_123'); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/retentions/ret_123'); + }); - public function testStampDraftUsesStampEndpoint(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], '{"id":"ret_123","status":"valid"}')); - $retentions = new Retentions('sk_test_abc123', ['httpClient' => $httpClient]); + test('sendByEmail sends email to customer', function () { + mockResponse('{"success":true}', 200); + $retentions = test()->facturapi->retentions(); - $retentions->stampDraft('ret_123'); + $result = $retentions->sendByEmail('ret_123', ['user@example.com']); - $request = $httpClient->requests()[0]; - self::assertSame('POST', $request->getMethod()); - self::assertSame('https://www.facturapi.io/v2/retentions/ret_123/stamp', (string) $request->getUri()); - self::assertSame('', (string) $request->getBody()); - } + expect($result->success)->toBeTrue(); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('POST'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/retentions/ret_123/email'); + expect(json_decode((string) $request->getBody(), true))->toBe(['email' => ['user@example.com']]); + }); - public function testCancelCanDeleteDraftWithoutQueryParameters(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], '{"id":"ret_123","deleted":true}')); - $retentions = new Retentions('sk_test_abc123', ['httpClient' => $httpClient]); + test('downloadPdf downloads pdf bytes', function () { + mockResponse('PDF_BYTES', 200); + $retentions = test()->facturapi->retentions(); - $retentions->cancel('ret_123'); + $result = $retentions->downloadPdf('ret_123'); + + expect($result)->toBe('PDF_BYTES'); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/retentions/ret_123/pdf'); + }); + + test('downloadXml downloads xml bytes', function () { + mockResponse('XML_BYTES', 200); + $retentions = test()->facturapi->retentions(); + + $result = $retentions->downloadXml('ret_123'); + + expect($result)->toBe('XML_BYTES'); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/retentions/ret_123/xml'); + }); - $request = $httpClient->requests()[0]; - self::assertSame('DELETE', $request->getMethod()); - self::assertSame('https://www.facturapi.io/v2/retentions/ret_123', (string) $request->getUri()); - } + test('downloadZip downloads zip bytes', function () { + mockResponse('ZIP_BYTES', 200); + $retentions = test()->facturapi->retentions(); - public function testAllCanFilterByDraftStatus(): void - { - $httpClient = new FakeHttpClient(new Response(200, [], '{"data":[]}')); - $retentions = new Retentions('sk_test_abc123', ['httpClient' => $httpClient]); + $result = $retentions->downloadZip('ret_123'); + + expect($result)->toBe('ZIP_BYTES'); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/retentions/ret_123/zip'); + }); + test('copy to draft uses copy endpoint', function () { + mockResponse('{"id":"ret_draft"}', 200); + $retentions = test()->facturapi->retentions(); + + $retentions->copyToDraft('ret_123'); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('POST'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/retentions/ret_123/copy'); + expect((string) $request->getBody())->toBe(''); + }); + test('stamp draft uses stamp endpoint', function () { + mockResponse('{"id":"ret_123","status":"valid"}', 200); + $retentions = test()->facturapi->retentions(); + + $retentions->stampDraft('ret_123'); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('POST'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/retentions/ret_123/stamp'); + expect((string) $request->getBody())->toBe(''); + }); + test('cancel can delete draft without query parameters', function () { + mockResponse('{"id":"ret_123","deleted":true}', 200); + $retentions = test()->facturapi->retentions(); + + $retentions->cancel('ret_123'); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('DELETE'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/retentions/ret_123'); + }); + test('all can filter by draft status', function () { + mockResponse('{"data":[]}', 200); + $retentions = test()->facturapi->retentions(); $retentions->all([ 'status' => 'draft', ]); - $request = $httpClient->requests()[0]; - self::assertSame('GET', $request->getMethod()); - self::assertSame('https://www.facturapi.io/v2/retentions?status=draft', (string) $request->getUri()); - } -} + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/retentions?status=draft'); + }); + +}); diff --git a/tests/Resources/ToolsTest.php b/tests/Resources/ToolsTest.php new file mode 100644 index 0000000..3df54d2 --- /dev/null +++ b/tests/Resources/ToolsTest.php @@ -0,0 +1,30 @@ +facturapi->tools(); + + $result = $tools->validateTaxId('ABC010101XYZ'); + + expect($result->is_valid)->toBeTrue(); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect(urldecode((string) $request->getUri()))->toBe('https://www.facturapi.io/v2/tools/tax_id_validation?tax_id=ABC010101XYZ'); + }); + + test('check hits health check endpoint', function () { + mockResponse('{"ok":true}', 200); + $tools = test()->facturapi->tools(); + + $result = $tools->check(); + + expect($result->ok)->toBeTrue(); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/check'); + }); +}); diff --git a/tests/Resources/WebhooksTest.php b/tests/Resources/WebhooksTest.php index 2d16234..3daad4f 100644 --- a/tests/Resources/WebhooksTest.php +++ b/tests/Resources/WebhooksTest.php @@ -1,20 +1,10 @@ $httpClient]); + $webhooks = test()->facturapi->webhooks(); $rawPayload = '{"id":"evt_123"}'; $secret = 'whsec_test_secret'; @@ -26,14 +16,12 @@ public function testValidateSignatureUsesLocalVerificationByDefault(): void '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]); + expect($result['valid'])->toBeTrue(); + expect(test()->httpClient->requests())->toHaveCount(0); + }); + test('validate signature falls back to api endpoint when local cannot run', function () { + mockResponse('{"valid":true}', 200); + $webhooks = test()->facturapi->webhooks(); $payload = [ 'body' => '{"id":"evt_123"}', @@ -43,19 +31,17 @@ public function testValidateSignatureFallsBackToApiEndpointWhenLocalCannotRun(): $result = $webhooks->validateSignature($payload); - self::assertTrue($result->valid); + expect($result['valid'])->toBeTrue(); - $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()); - } + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('POST'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/webhooks/validate-signature'); + expect($request->getHeaderLine('Content-Type'))->toBe('application/json'); + expect((string) $request->getBody())->toBe(json_encode($payload)); + }); + test('validate signature local supports raw hex and rejects invalid signatures', function () { - public function testValidateSignatureLocalSupportsRawHexAndRejectsInvalidSignatures(): void - { - $httpClient = new FakeHttpClient(); - $webhooks = new Webhooks('sk_test_abc123', ['httpClient' => $httpClient]); + $webhooks = test()->facturapi->webhooks(); $rawPayload = '{"id":"evt_456"}'; $secret = 'whsec_test_secret'; @@ -72,8 +58,94 @@ public function testValidateSignatureLocalSupportsRawHexAndRejectsInvalidSignatu 'secret' => $secret, ]); - self::assertTrue($valid->valid); - self::assertFalse($invalid->valid); - self::assertCount(0, $httpClient->requests()); - } -} + expect($valid['valid'])->toBeTrue(); + expect($invalid['valid'])->toBeFalse(); + expect(test()->httpClient->requests())->toHaveCount(0); + }); + + + test('create sends json body with url and events', function () { + mockResponse('{"id":"wh_123"}', 201); + $webhooks = test()->facturapi->webhooks(); + + $payload = [ + 'url' => 'https://example.com/webhook', + 'enabled_events' => ['invoice.created', 'invoice.paid'] + ]; + + $result = $webhooks->create($payload); + + expect($result->id)->toBe('wh_123'); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('POST'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/webhooks'); + expect(json_decode((string) $request->getBody(), true))->toBe($payload); + }); + + test('update sends json body with updated properties', function () { + mockResponse(json_encode([ + 'id' => 'wh_123', + 'created_at' => '2025-03-01T12:00:00Z', + 'livemode' => false, + 'organization' => 'org_123', + 'url' => 'https://example.com/webhook-updated', + 'status' => 'active', + 'enabled_events' => ['invoice.created'], + ]), 200); + $webhooks = test()->facturapi->webhooks(); + + $payload = [ + 'url' => 'https://example.com/webhook-updated', + 'status' => 'active', + 'enabled_events' => ['invoice.created'], + ]; + + $result = $webhooks->update('wh_123', $payload); + + expect($result->id)->toBe('wh_123'); + expect($result->url)->toBe('https://example.com/webhook-updated'); + expect($result->status)->toBe('active'); + expect($result->enabled_events)->toBe(['invoice.created']); + expect($result->organization)->toBe('org_123'); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('PUT'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/webhooks/wh_123'); + expect(json_decode((string) $request->getBody(), true))->toBe($payload); + }); + + test('all retrieves list of webhooks', function () { + mockResponse('{"data":[]}', 200); + $webhooks = test()->facturapi->webhooks(); + + $webhooks->all(); + + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/webhooks'); + }); + + test('retrieve fetches a single webhook by id', function () { + mockResponse('{"id":"wh_123"}', 200); + $webhooks = test()->facturapi->webhooks(); + + $result = $webhooks->retrieve('wh_123'); + + expect($result->id)->toBe('wh_123'); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('GET'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/webhooks/wh_123'); + }); + + test('delete removes a webhook by id', function () { + mockResponse('{"id":"wh_123"}', 200); + $webhooks = test()->facturapi->webhooks(); + + $result = $webhooks->delete('wh_123'); + + expect($result->id)->toBe('wh_123'); + $request = test()->httpClient->requests()[0]; + expect($request->getMethod())->toBe('DELETE'); + expect((string) $request->getUri())->toBe('https://www.facturapi.io/v2/webhooks/wh_123'); + }); +}); diff --git a/tests/Support/FakeHttpClient.php b/tests/Support/FakeHttpClient.php index 6bfc0d7..eaaf55c 100644 --- a/tests/Support/FakeHttpClient.php +++ b/tests/Support/FakeHttpClient.php @@ -37,6 +37,11 @@ public function sendRequest(RequestInterface $request): ResponseInterface } /** @return list */ + public function addResponse(ResponseInterface $response): void + { + $this->responses[] = $response; + } + public function requests(): array { return $this->requests; diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..0592d29 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,23 @@ +httpClient = new FakeHttpClient(); + $this->facturapi = new Facturapi('sk_test_abc123', ['httpClient' => $this->httpClient]); + } +}